Compare commits
3 Commits
183f6af976
...
c0765b7dd0
Author | SHA1 | Date |
---|---|---|
anna | c0765b7dd0 | 7 months ago |
anna | 3173e99797 | 7 months ago |
anna | 9c060164ae | 7 months ago |
@ -1,7 +1,18 @@
|
||||
[package]
|
||||
name = "uwui"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"dsl"
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
repository = "https://git.bsd.gay/fef/uwui"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
uwui_dsl = { path = "./uwui_dsl" }
|
||||
uwui-dsl = { path = "./dsl" }
|
||||
|
@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "uwui-dsl"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0.67"
|
||||
quote = "1.0.33"
|
||||
syn = "2.0.37"
|
@ -0,0 +1,45 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
|
||||
pub fn invoke(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
let name = input.ident;
|
||||
|
||||
let expanded = quote! {
|
||||
#[automatically_derived]
|
||||
impl<B: crate::backend::Backend>
|
||||
crate::view::custom::CustomComponent<B> for #name
|
||||
{}
|
||||
|
||||
#[automatically_derived]
|
||||
impl<B: crate::backend::Backend>
|
||||
crate::view::Construct<B> for #name
|
||||
{
|
||||
type Params = <Self as crate::view::custom::NewCustom<B>>::Params;
|
||||
|
||||
fn construct(params: Self::Params) -> Self {
|
||||
<Self as crate::view::custom::NewCustom<B>>::new(params)
|
||||
}
|
||||
|
||||
fn into_view(self) -> crate::view::SomeView<B> {
|
||||
crate::view::SomeView::<B>::Custom(Box::new(self))
|
||||
}
|
||||
}
|
||||
|
||||
#[automatically_derived]
|
||||
impl<B: crate::backend::Backend>
|
||||
crate::view::ConstructGroup<B> for #name
|
||||
where
|
||||
#name : crate::view::custom::NewCustomGroup<B>,
|
||||
{
|
||||
fn construct_group<'a, I>(params: Self::Params, children: I) -> Self
|
||||
where
|
||||
I: Iterator<Item = &'a crate::view::View<B>>,
|
||||
{
|
||||
<Self as crate::view::custom::NewCustomGroup<B>>::new_group(params, children)
|
||||
}
|
||||
}
|
||||
};
|
||||
expanded.into()
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
pub mod custom_component;
|
||||
pub mod native_component;
|
@ -0,0 +1,49 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
|
||||
pub fn invoke(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
let name = input.ident;
|
||||
|
||||
let expanded = quote! {
|
||||
#[automatically_derived]
|
||||
impl<B: crate::backend::Backend>
|
||||
crate::view::NativeComponent<B> for #name
|
||||
where
|
||||
#name : crate::backend::Render<B>,
|
||||
{}
|
||||
|
||||
#[automatically_derived]
|
||||
impl<B: crate::backend::Backend>
|
||||
crate::view::Construct<B> for #name
|
||||
where
|
||||
#name : crate::backend::Render<B>,
|
||||
{
|
||||
type Params = <Self as crate::view::NewNative<B>>::Params;
|
||||
|
||||
fn construct(params: Self::Params) -> Self {
|
||||
<Self as crate::view::NewNative<B>>::new(params)
|
||||
}
|
||||
|
||||
fn into_view(self) -> crate::view::SomeView<B> {
|
||||
crate::view::SomeView::<B>::Native(Box::new(self))
|
||||
}
|
||||
}
|
||||
|
||||
#[automatically_derived]
|
||||
impl<B: crate::backend::Backend>
|
||||
crate::view::ConstructGroup<B> for #name
|
||||
where
|
||||
#name : crate::view::NewNativeGroup<B> + crate::view::Render<B>,
|
||||
{
|
||||
fn construct_group<'a, I>(params: Self::Params, children: I) -> Self
|
||||
where
|
||||
I: Iterator<Item = &'a View<B>>,
|
||||
{
|
||||
<Self as crate::view::NewNativeGroup<B>>::new_group(params, children)
|
||||
}
|
||||
}
|
||||
};
|
||||
expanded.into()
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
#![feature(proc_macro_diagnostic)]
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
mod derive;
|
||||
mod template;
|
||||
|
||||
#[proc_macro]
|
||||
pub fn template(input: TokenStream) -> TokenStream {
|
||||
template::invoke(input)
|
||||
}
|
||||
|
||||
#[proc_macro_derive(NativeComponent)]
|
||||
pub fn native_component(input: TokenStream) -> TokenStream {
|
||||
derive::native_component::invoke(input)
|
||||
}
|
||||
|
||||
#[proc_macro_derive(CustomComponent)]
|
||||
pub fn custom_component(input: TokenStream) -> TokenStream {
|
||||
derive::custom_component::invoke(input)
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
use crate::template::Params;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{quote, ToTokens};
|
||||
|
||||
use super::{Body, Component, NamedParam};
|
||||
|
||||
impl ToTokens for Body {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let elements = self.elements.iter();
|
||||
let code = quote! {
|
||||
#(#elements),*
|
||||
};
|
||||
tokens.extend(code);
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Component {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let name = &self.name;
|
||||
let params = match self.params.as_ref() {
|
||||
Some(p) => match p {
|
||||
Params::Anon(params) => quote! {
|
||||
::core::convert::Into::<<#name as crate::view::Construct<_>>::Params>::into(
|
||||
(#(::core::convert::Into::<_>::into(#params)),*,)
|
||||
)
|
||||
},
|
||||
Params::Named(params) => quote! {
|
||||
{
|
||||
let mut __uwui_params = <
|
||||
<#name as crate::view::Construct::<_>>::Params as ::core::default::Default
|
||||
>::default();
|
||||
#(__uwui_params.#params)*
|
||||
__uwui_params
|
||||
}
|
||||
},
|
||||
},
|
||||
None => quote! {
|
||||
<<#name as crate::view::Construct<_>>::Params as ::core::default::Default>::default()
|
||||
},
|
||||
};
|
||||
|
||||
let initializer = if let Some(body) = self.body.as_ref() {
|
||||
let body = body.elements.as_slice();
|
||||
quote! {
|
||||
crate::view::View::<_>::new_group::<#name, _>(
|
||||
#params,
|
||||
[
|
||||
#(#body),*
|
||||
]
|
||||
)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
crate::view::View::<_>::new::<#name>(#params)
|
||||
}
|
||||
};
|
||||
|
||||
tokens.extend(initializer);
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for NamedParam {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let (name, val) = (&self.name, &self.val);
|
||||
let code = quote! {
|
||||
#name = ::core::convert::Into::<_>::into(#val);
|
||||
};
|
||||
tokens.extend(code);
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, token, Expr, Ident, Path};
|
||||
|
||||
mod compile;
|
||||
mod parse;
|
||||
|
||||
pub fn invoke(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as Body);
|
||||
let output = quote! {
|
||||
match (()) {
|
||||
(()) => {
|
||||
<
|
||||
crate::view::Template as ::core::iter::FromIterator<crate::view::View<_>>
|
||||
>::from_iter([
|
||||
#input
|
||||
])
|
||||
}
|
||||
}
|
||||
};
|
||||
output.into()
|
||||
}
|
||||
|
||||
struct Body {
|
||||
elements: Vec<Component>,
|
||||
}
|
||||
|
||||
struct Component {
|
||||
name: Path,
|
||||
params: Option<Params>,
|
||||
body: Option<Body>,
|
||||
}
|
||||
|
||||
enum Params {
|
||||
/// `name: value` style parameter list
|
||||
Named(Vec<NamedParam>),
|
||||
/// tuple style unnamed parameter list
|
||||
Anon(Vec<Expr>),
|
||||
}
|
||||
|
||||
struct NamedParam {
|
||||
pub name: Ident,
|
||||
pub colon: token::Colon,
|
||||
pub val: Expr,
|
||||
}
|
||||
|
||||
struct EventHandler {
|
||||
pub name: Ident,
|
||||
pub callback: Expr,
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
use crate::template::Params;
|
||||
use syn::{
|
||||
braced, parenthesized,
|
||||
parse::{discouraged::Speculative, Parse, ParseStream},
|
||||
token, Expr, Path, Token,
|
||||
};
|
||||
|
||||
use super::{Body, Component, NamedParam};
|
||||
|
||||
impl Parse for Body {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let mut elements = Vec::new();
|
||||
while !input.is_empty() {
|
||||
elements.push(input.parse()?);
|
||||
}
|
||||
Ok(Self { elements })
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for Component {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let name: Path = input.parse()?;
|
||||
|
||||
let params = if input.lookahead1().peek(token::Paren) {
|
||||
Some(Params::parse(input)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let body = if input.lookahead1().peek(token::Brace) {
|
||||
let inner;
|
||||
braced!(inner in input);
|
||||
Some(inner.parse()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self { name, params, body })
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for Params {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let inner;
|
||||
parenthesized!(inner in input);
|
||||
if inner.is_empty() {
|
||||
Ok(Self::Named(Vec::new()))
|
||||
} else {
|
||||
let ahead = inner.fork();
|
||||
match ahead.parse_terminated(NamedParam::parse, Token![,]) {
|
||||
Ok(vals) => {
|
||||
let x = Ok(Self::Named(vals.into_iter().collect()));
|
||||
inner.advance_to(&ahead);
|
||||
x
|
||||
}
|
||||
Err(_) => {
|
||||
let vals = inner.parse_terminated(Expr::parse, Token![,])?;
|
||||
Ok(Self::Anon(vals.into_iter().collect()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for NamedParam {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let name: syn::Ident = input.parse()?;
|
||||
if name.to_string().starts_with("__uwui") {
|
||||
// TODO: we need to walk through the entire syntax tree, not just param names
|
||||
return Err(syn::Error::new(
|
||||
name.span(),
|
||||
"Names starting with `__uwui` are reserved",
|
||||
));
|
||||
}
|
||||
let colon = input.parse()?;
|
||||
let val = input.parse()?;
|
||||
|
||||
Ok(Self { name, colon, val })
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
use proc_macro::{Diagnostic, Level, Span};
|
||||
use proc_macro2::TokenStream;
|
||||
|
||||
pub trait Compile: Sized {
|
||||
fn compile(self) -> TokenStream;
|
||||
}
|
||||
|
||||
pub fn emit_err(span: impl Into<Option<Span>>, msg: impl Into<String>) {
|
||||
emit_diagnostic(span.into(), Level::Error, msg)
|
||||
}
|
||||
|
||||
pub fn emit_warning(span: impl Into<Option<Span>>, msg: impl Into<String>) {
|
||||
emit_diagnostic(span.into(), Level::Warning, msg)
|
||||
}
|
||||
|
||||
pub fn emit_note(span: impl Into<Option<Span>>, msg: impl Into<String>) {
|
||||
emit_diagnostic(span.into(), Level::Note, msg)
|
||||
}
|
||||
|
||||
pub fn emit_help(span: impl Into<Option<Span>>, msg: impl Into<String>) {
|
||||
emit_diagnostic(span.into(), Level::Help, msg)
|
||||
}
|
||||
|
||||
fn emit_diagnostic(span: Option<Span>, level: Level, msg: impl Into<String>) {
|
||||
let diagnostic = match span {
|
||||
Some(span) => Diagnostic::spanned(span, level, msg),
|
||||
None => Diagnostic::new(level, msg),
|
||||
};
|
||||
diagnostic.emit();
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
/// Specifies the alignment of a view's content.
|
||||
#[derive(Default)]
|
||||
pub enum Gravity {
|
||||
/// Align content to the start of the view.
|
||||
/// In horizontal layouts, this is the left side for L2R layouts and the
|
||||
/// right side for R2L layouts. In vertical layouts, it is the top.
|
||||
#[default]
|
||||
Start,
|
||||
/// Align content to the center of the view.
|
||||
Center,
|
||||
/// Align content to the end of the view.
|
||||
/// In horizontal layouts, this is the right side for L2R layouts and the
|
||||
/// left side for R2L layouts. In vertical layouts, it is the bottom.
|
||||
End,
|
||||
}
|
@ -1,15 +1,43 @@
|
||||
pub mod backend;
|
||||
pub mod layout;
|
||||
pub mod view;
|
||||
|
||||
pub use uwui_dsl::template;
|
||||
pub use uwui_dsl::{template, CustomComponent, NativeComponent};
|
||||
|
||||
use view::{stack::Gravity, Button, DynView, Text, VStack};
|
||||
use layout::Gravity;
|
||||
use view::{Button, GetTemplate, NewCustom, Template, Text, VStack};
|
||||
|
||||
fn asdf() -> Vec<DynView> {
|
||||
template!(
|
||||
VStack(gravity: Gravity::Center, homogay: true) {
|
||||
Text(content: "hello, world")
|
||||
fn asdf() -> Template {
|
||||
template! {
|
||||
VStack(Gravity::Center) {
|
||||
MyComponent(x: 420)
|
||||
Button(label: "don't click me")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MyParams {
|
||||
x: i32,
|
||||
}
|
||||
|
||||
#[derive(CustomComponent)]
|
||||
struct MyComponent {
|
||||
params: MyParams,
|
||||
}
|
||||
|
||||
impl<B: backend::Backend> NewCustom<B> for MyComponent {
|
||||
type Params = MyParams;
|
||||
|
||||
fn new(params: Self::Params) -> Self {
|
||||
Self { params }
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: backend::Backend> GetTemplate<B> for MyComponent {
|
||||
fn get_template(&self) -> Template {
|
||||
template! {
|
||||
Text(format!("x = {}", self.params.x))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,51 @@
|
||||
use std::any::Any;
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use crate::backend::{Backend, Render, TargetBackend};
|
||||
use crate::view::{Construct, SomeView, Template, View};
|
||||
|
||||
pub trait NewCustom<B: Backend = TargetBackend>: Sized {
|
||||
type Params: Default;
|
||||
|
||||
fn new(params: Self::Params) -> Self;
|
||||
}
|
||||
|
||||
pub trait NewCustomGroup<B: Backend = TargetBackend>: NewCustom<B> {
|
||||
fn new_group<'a, I>(params: Self::Params, children: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = &'a View<B>>;
|
||||
}
|
||||
|
||||
pub trait GetTemplate<B: Backend = TargetBackend> {
|
||||
fn get_template(&self) -> Template;
|
||||
}
|
||||
|
||||
pub trait CustomComponent<B: Backend = TargetBackend>: Any + Send + GetTemplate {}
|
||||
|
||||
pub struct ComponentWrapper<C: CustomComponent<B>, B: Backend = TargetBackend> {
|
||||
component: C,
|
||||
_backend: PhantomData<B>,
|
||||
}
|
||||
|
||||
impl<C, B> Deref for ComponentWrapper<C, B>
|
||||
where
|
||||
B: Backend,
|
||||
C: CustomComponent<B>,
|
||||
{
|
||||
type Target = C;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.component
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, B> DerefMut for ComponentWrapper<C, B>
|
||||
where
|
||||
B: Backend,
|
||||
C: CustomComponent<B>,
|
||||
{
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.component
|
||||
}
|
||||
}
|
@ -1,39 +1,36 @@
|
||||
use crate::backend::{Backend, Render};
|
||||
use crate::view::{DynView, Id, NewView, View};
|
||||
use uwui_dsl::NativeComponent;
|
||||
|
||||
use crate::backend::Backend;
|
||||
use crate::layout::Gravity;
|
||||
use crate::view::{NativeComponent, NewNative};
|
||||
|
||||
#[derive(NativeComponent)]
|
||||
pub struct Text {
|
||||
id: Id,
|
||||
content: String,
|
||||
params: TextParams,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TextParams {
|
||||
pub content: String,
|
||||
pub gravity: Gravity,
|
||||
}
|
||||
|
||||
impl<B: Backend> View<B> for Text
|
||||
where
|
||||
Text: Render<B>,
|
||||
{
|
||||
fn id(&self) -> Id {
|
||||
self.id
|
||||
impl From<(String,)> for TextParams {
|
||||
fn from(value: (String,)) -> Self {
|
||||
Self {
|
||||
content: value.0,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: Backend> NewView<B> for Text
|
||||
impl<B: Backend> NewNative<B> for Text
|
||||
where
|
||||
Text: Render<B>,
|
||||
Text: NativeComponent<B>,
|
||||
{
|
||||
type Params = TextParams;
|
||||
|
||||
fn new(id: Id, params: Self::Params) -> Self {
|
||||
Self {
|
||||
id,
|
||||
content: params.content,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_view(self) -> DynView<B> {
|
||||
Box::new(self)
|
||||
fn new(params: Self::Params) -> Self {
|
||||
Self { params }
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "uwui_dsl"
|
||||
version = "0.1.0"
|
@ -1,7 +0,0 @@
|
||||
[package]
|
||||
name = "uwui_dsl"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
@ -1,14 +0,0 @@
|
||||
; This grammar spec uses the ABNF notation as defined in RFC 5234.
|
||||
|
||||
template = space *component
|
||||
component = *listener name space [ param-list space ] [ child-list space ]
|
||||
listener = "@" ident space "(" space expr space ")" space
|
||||
name = [ path-sep space ] ident *( space path-sep space ident )
|
||||
path-sep = "::"
|
||||
param-list = "(" space *( param space "," space ) [ param space ] ")"
|
||||
param = ident space ":" space expr
|
||||
child-list = "{" space *( component ) "}"
|
||||
|
||||
expr = <now that's just rust code, stop being a pedantic bitch about it>
|
||||
ident = <any valid rust identifier, excluding those starting with "__uwui">
|
||||
space = *( <anything that rust considers whitespace, including comments> )
|
@ -1,215 +0,0 @@
|
||||
use proc_macro::{Delimiter, Group, Ident, Punct, Spacing, TokenStream, TokenTree};
|
||||
|
||||
use crate::parser::{Component, Name, Param};
|
||||
use crate::TtIter;
|
||||
|
||||
impl Component {
|
||||
pub(crate) fn compile(self) -> impl TtIter {
|
||||
// wrap into `<Name as $crate::view::NewView<_>>::into_view()`
|
||||
self.name
|
||||
.clone()
|
||||
.into_new_view()
|
||||
.chain(syms::parse("::into_view"))
|
||||
.chain([TokenTree::from(Group::new(
|
||||
Delimiter::Parenthesis,
|
||||
TokenStream::from_iter(self.compile_inner()),
|
||||
))])
|
||||
}
|
||||
|
||||
pub(crate) fn compile_inner(self) -> impl TtIter {
|
||||
let span = self.name.span();
|
||||
|
||||
self.name
|
||||
.clone()
|
||||
.into_constructor(self.children.is_some())
|
||||
.chain([Group::new(
|
||||
Delimiter::Parenthesis,
|
||||
TokenStream::from_iter(
|
||||
syms::id_new_call(span)
|
||||
.chain([
|
||||
Punct::new(',', Spacing::Alone).into(),
|
||||
Self::compile_params(self.name, self.params),
|
||||
])
|
||||
.chain(self.children.into_iter().flat_map(|children| {
|
||||
syms::parse(", ::std::vec::Vec::<_>::from").chain([Group::new(
|
||||
Delimiter::Parenthesis,
|
||||
Self::compile_children(children).into(),
|
||||
)
|
||||
.into()])
|
||||
})),
|
||||
),
|
||||
)
|
||||
.into()])
|
||||
}
|
||||
|
||||
/// `Name(key1: val1)` ->
|
||||
///
|
||||
/// ```
|
||||
/// {
|
||||
/// let mut __uwui_params = <Name as ::uwui::view::NewView>::Params::default();
|
||||
/// __uwui_params.key1 = ::core::convert::Into::<_>::into(val1);
|
||||
/// __uwui_params
|
||||
/// }
|
||||
/// ```
|
||||
fn compile_params(name: Name, params: Option<Vec<Param>>) -> TokenTree {
|
||||
let span = name.span();
|
||||
|
||||
Group::new(
|
||||
Delimiter::Brace,
|
||||
TokenStream::from_iter(
|
||||
[
|
||||
TokenTree::Ident(Ident::new("let", span)),
|
||||
Ident::new("mut", span).into(),
|
||||
Ident::new("__uwui_params", span).into(),
|
||||
Punct::new('=', Spacing::Alone).into(),
|
||||
]
|
||||
.into_iter()
|
||||
.chain(name.into_params_type())
|
||||
.chain([
|
||||
Punct::new(':', Spacing::Joint).into(),
|
||||
Punct::new(':', Spacing::Alone).into(),
|
||||
Ident::new("default", span).into(),
|
||||
Group::new(Delimiter::Parenthesis, TokenStream::new()).into(),
|
||||
Punct::new(';', Spacing::Alone).into(),
|
||||
])
|
||||
.chain(
|
||||
params
|
||||
.unwrap_or(Vec::new())
|
||||
.into_iter()
|
||||
.flat_map(|param| param.compile(Ident::new("__uwui_params", span))),
|
||||
)
|
||||
.chain([Ident::new("__uwui_params", span).into()]),
|
||||
),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn compile_children(children: Vec<Component>) -> TokenTree {
|
||||
Group::new(
|
||||
Delimiter::Bracket,
|
||||
TokenStream::from_iter(children.into_iter().flat_map(|child| {
|
||||
child
|
||||
.compile()
|
||||
.chain([Punct::new(',', Spacing::Alone).into()])
|
||||
})),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Name {
|
||||
/// `Name -> <Name as $crate::view::NewView<_>>::new` (!group)
|
||||
/// `Name -> <Name as $crate::view::NewViewGroup<_>>::new_group` (group)
|
||||
fn into_constructor(self, group: bool) -> impl TtIter {
|
||||
let span = self.span();
|
||||
let trait_name = if group { "NewViewGroup" } else { "NewView" };
|
||||
let fn_name = if group { "new_group" } else { "new" };
|
||||
|
||||
[TokenTree::Punct(Punct::new('<', Spacing::Alone))]
|
||||
.into_iter()
|
||||
.chain(self.0.into_iter())
|
||||
.chain([Ident::new("as", span).into()])
|
||||
.chain(syms::absolute_path(span, ["view", trait_name].into_iter()))
|
||||
.chain([
|
||||
Punct::new('<', Spacing::Alone).into(),
|
||||
Ident::new("_", span).into(),
|
||||
Punct::new('>', Spacing::Joint).into(),
|
||||
Punct::new('>', Spacing::Joint).into(),
|
||||
Punct::new(':', Spacing::Joint).into(),
|
||||
Punct::new(':', Spacing::Alone).into(),
|
||||
Ident::new(fn_name, span).into(),
|
||||
])
|
||||
}
|
||||
|
||||
/// `Name -> <Name as $crate::view::NewView::<_>>::Params`
|
||||
fn into_params_type(self) -> impl TtIter {
|
||||
let span = self.span();
|
||||
|
||||
self.into_new_view().chain([
|
||||
Punct::new(':', Spacing::Joint).into(),
|
||||
Punct::new(':', Spacing::Alone).into(),
|
||||
Ident::new("Params", span).into(),
|
||||
])
|
||||
}
|
||||
|
||||
/// `Name -> <Name as $crate::view::NewView<_>>`
|
||||
fn into_new_view(self) -> impl TtIter {
|
||||
let span = self.span();
|
||||
|
||||
[Punct::new('<', Spacing::Alone).into()]
|
||||
.into_iter()
|
||||
.chain(self.0.into_iter())
|
||||
.chain([Ident::new("as", span).into()])
|
||||
.chain(syms::new_view(span))
|
||||
.chain([Punct::new('>', Spacing::Joint).into()])
|
||||
}
|
||||
}
|
||||
|
||||
impl Param {
|
||||
/// `name: expr -> ident.name = ::core::convert::Into::<_>::into(expr);`
|
||||
fn compile(self, ident: Ident) -> impl TtIter {
|
||||
let (name, colon, expr) = (self.0, self.1, self.2);
|
||||
|
||||
let mut eq = Punct::new('=', Spacing::Alone);
|
||||
eq.set_span(colon.span());
|
||||
|
||||
[
|
||||
ident.into(),
|
||||
Punct::new('.', Spacing::Alone).into(),
|
||||
name,
|
||||
eq.into(),
|
||||
]
|
||||
.into_iter()
|
||||
.chain(syms::parse("::core::convert::Into::<_>::into"))
|
||||
.chain([
|
||||
Group::new(
|
||||
Delimiter::Parenthesis,
|
||||
TokenStream::from_iter(expr.0.into_iter()),
|
||||
)
|
||||
.into(),
|
||||
Punct::new(';', Spacing::Alone).into(),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
/// Helpers for static identifiers within our API
|
||||
mod syms {
|
||||
use proc_macro::{Delimiter, Group, Ident, Punct, Spacing, Span, TokenStream};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::TtIter;
|
||||
|
||||
/// `$crate::view::Id::new()`
|
||||
pub(super) fn id_new_call(span: Span) -> impl TtIter {
|
||||
absolute_path(span, ["view", "Id", "new"].into_iter()).chain([Group::new(
|
||||
Delimiter::Parenthesis,
|
||||
TokenStream::new(),
|
||||
)
|
||||
.into()])
|
||||
}
|
||||
|
||||
/// `$crate::view::NewView::<_>`
|
||||
pub(super) fn new_view(span: Span) -> impl TtIter {
|
||||
absolute_path(span, ["view", "NewView"].into_iter()).chain(parse("::<_>"))
|
||||
}
|
||||
|
||||
pub(super) fn absolute_path<'a>(
|
||||
span: Span,
|
||||
segments: impl Iterator<Item = &'a str> + 'a,
|
||||
) -> impl TtIter + 'a {
|
||||
[Ident::new("crate", span).into()]
|
||||
.into_iter()
|
||||
.chain(segments.flat_map(move |name| {
|
||||
[
|
||||
Punct::new(':', Spacing::Joint).into(),
|
||||
Punct::new(':', Spacing::Alone).into(),
|
||||
Ident::new(name, span).into(),
|
||||
]
|
||||
.into_iter()
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) fn parse(code: &str) -> impl TtIter {
|
||||
TokenStream::from_str(code).unwrap().into_iter()
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
#![feature(proc_macro_diagnostic)]
|
||||
#![feature(proc_macro_span)]
|
||||
|
||||
use proc_macro::{Delimiter, Group, Punct, Spacing, TokenStream, TokenTree};
|
||||
use std::str::FromStr;
|
||||
|
||||
pub(crate) trait TtIter: Iterator<Item = TokenTree> {}
|
||||
impl<T> TtIter for T where T: Iterator<Item = TokenTree> {}
|
||||
|
||||
mod compiler;
|
||||
mod parser;
|
||||
|
||||
use parser::Parser;
|
||||
|
||||
/// Build a view tree out of a template written in the uwui DSL.
|
||||
#[proc_macro]
|
||||
pub fn template(input: TokenStream) -> TokenStream {
|
||||
let mut stream = input.into_iter().peekable();
|
||||
let parser = Parser::from(&mut stream);
|
||||
let array = TokenTree::Group(Group::new(
|
||||
Delimiter::Bracket,
|
||||
TokenStream::from_iter(parser.flat_map(|component| {
|
||||
component
|
||||
.compile()
|
||||
.chain([Punct::new(',', Spacing::Alone).into()])
|
||||
})),
|
||||
));
|
||||
|
||||
let mut output = TokenStream::from_str("::std::vec::Vec::<_>::from").unwrap();
|
||||
output.extend(TokenStream::from(TokenTree::Group(Group::new(
|
||||
Delimiter::Parenthesis,
|
||||
array.into(),
|
||||
))));
|
||||
output = TokenStream::from(TokenTree::Group(Group::new(Delimiter::Brace, output)));
|
||||
output
|
||||
}
|
@ -1,388 +0,0 @@
|
||||
use crate::TtIter;
|
||||
use proc_macro::{
|
||||
Delimiter, Diagnostic, Group, Ident, Level, Spacing, Span, TokenStream, TokenTree,
|
||||
};
|
||||
use std::{fmt, iter::Peekable};
|
||||
|
||||
/// A single component definition, including children.
|
||||
pub(crate) struct Component {
|
||||
/// Event listeners attached to this component.
|
||||
pub(crate) listeners: Vec<Listener>,
|
||||
/// The type name of this component (may be namespaced).
|
||||
pub(crate) name: Name,
|
||||
/// The parameter list as a dictionary, if `name` is followed by parentheses.
|
||||
pub(crate) params: Option<Vec<Param>>,
|
||||
/// All child component definitions.
|
||||
pub(crate) children: Option<Vec<Component>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Name(pub(crate) Vec<TokenTree>);
|
||||
pub(crate) struct Param(pub(crate) TokenTree, pub(crate) TokenTree, pub(crate) Expr);
|
||||
pub(crate) struct Expr(pub(crate) Vec<TokenTree>);
|
||||
pub(crate) struct Listener {
|
||||
pub(crate) at: TokenTree,
|
||||
pub(crate) name: TokenTree,
|
||||
pub(crate) callback: TokenTree,
|
||||
}
|
||||
|
||||
/// The main syntax tree parser. This implements [`Iterator`], so it will
|
||||
/// only parse (and consume the input iterator) when consumed itself.
|
||||
pub(crate) struct Parser<'a, T: TtIter> {
|
||||
stream: &'a mut Peekable<T>,
|
||||
}
|
||||
|
||||
impl<'a, T: TtIter> From<&'a mut Peekable<T>> for Parser<'a, T> {
|
||||
fn from(stream: &'a mut Peekable<T>) -> Self {
|
||||
Self { stream }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: TtIter> Iterator for Parser<'a, T> {
|
||||
type Item = Component;
|
||||
|
||||
fn next(&mut self) -> Option<Component> {
|
||||
self.parse_component()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: TtIter> Parser<'a, T> {
|
||||
/// ```notrust
|
||||
/// component = *listener name space [ param-list space ] [ child-list space ]
|
||||
/// param-list = "(" space *( param space "," space ) [ param space ] ")"
|
||||
/// child-list = "{" space *( component ) "}"
|
||||
/// ```
|
||||
fn parse_component(&mut self) -> Option<Component> {
|
||||
self.stream.peek()?;
|
||||
|
||||
let mut listeners = Vec::new();
|
||||
while let Some(listener) = Listener::parse(self.stream) {
|
||||
Diagnostic::spanned(
|
||||
listener.span(),
|
||||
Level::Error,
|
||||
"Event listeners aren't implemented yet",
|
||||
)
|
||||
.emit();
|
||||
listeners.push(listener);
|
||||
}
|
||||
|
||||
let name = Name::eat(self.stream)?;
|
||||
|
||||
let params = self
|
||||
.stream
|
||||
.next_if(is_group(Delimiter::Parenthesis))
|
||||
.map(|tt| match tt {
|
||||
TokenTree::Group(group) => group.stream().into_iter().peekable(),
|
||||
_ => unreachable!("is_group() already matched this"),
|
||||
})
|
||||
.map(|mut stream| {
|
||||
let mut list = Vec::new();
|
||||
while let Some(param) = Param::parse(&mut stream) {
|
||||
list.push(param);
|
||||
stream.next_if(is_punct(','));
|
||||
}
|
||||
if let Some(tt) = stream.peek() {
|
||||
Diagnostic::spanned(tt.span(), Level::Error, "Unexpected token").emit();
|
||||
}
|
||||
list
|
||||
});
|
||||
|
||||
let children = self
|
||||
.stream
|
||||
.next_if(is_group(Delimiter::Brace))
|
||||
.map(|tt| match tt {
|
||||
TokenTree::Group(group) => group.stream().into_iter().peekable(),
|
||||
_ => unreachable!("is_child_list already matched this"),
|
||||
})
|
||||
.map(|mut stream| Parser::from(&mut stream).collect());
|
||||
|
||||
Some(Component {
|
||||
listeners,
|
||||
name,
|
||||
params,
|
||||
children,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Name {
|
||||
/// ```notrust
|
||||
/// name = [ path-sep space ] ident *( space path-sep space ident )
|
||||
/// ```
|
||||
fn eat(stream: &mut Peekable<impl TtIter>) -> Option<Self> {
|
||||
stream.peek()?;
|
||||
let mut segments = Vec::new();
|
||||
|
||||
// [ path-sep ]
|
||||
segments.extend(
|
||||
Self::parse_path_sep(stream)
|
||||
.into_iter()
|
||||
.flat_map(|sep| sep.into_iter()),
|
||||
);
|
||||
|
||||
// ident
|
||||
segments.push(eat_ident(stream)?);
|
||||
|
||||
// *( path-sep ident )
|
||||
while let Some(sep) = Self::parse_path_sep(stream) {
|
||||
segments.extend(sep.into_iter());
|
||||
segments.push(eat_ident(stream)?);
|
||||
}
|
||||
|
||||
Some(Name(segments))
|
||||
}
|
||||
|
||||
pub(crate) fn span(&self) -> Span {
|
||||
let first = self.0.first().unwrap().span();
|
||||
let last = self.0.last().unwrap().span();
|
||||
first.join(last).unwrap()
|
||||
}
|
||||
|
||||
/// ```notrust
|
||||
/// path-sep = "::"
|
||||
/// ```
|
||||
fn parse_path_sep(stream: &mut Peekable<impl TtIter>) -> Option<[TokenTree; 2]> {
|
||||
let first = stream.next_if(is_punct_spaced(':', Spacing::Joint))?;
|
||||
let second = match stream.next() {
|
||||
Some(tt) => match &tt {
|
||||
TokenTree::Punct(punct)
|
||||
if punct.as_char() == ':' && punct.spacing() == Spacing::Alone =>
|
||||
{
|
||||
Some(tt)
|
||||
}
|
||||
_ => {
|
||||
Diagnostic::spanned(tt.span(), Level::Error, "Expected a `:`").emit();
|
||||
None
|
||||
}
|
||||
},
|
||||
None => {
|
||||
Diagnostic::new(Level::Error, "Unexpected end of stream").emit();
|
||||
None
|
||||
}
|
||||
}?;
|
||||
|
||||
Some([first, second])
|
||||
}
|
||||
}
|
||||
|
||||
impl Param {
|
||||
/// ```notrust
|
||||
/// param = ident space ":" space expr
|
||||
/// ```
|
||||
fn parse(stream: &mut Peekable<impl TtIter>) -> Option<Self> {
|
||||
stream.peek()?;
|
||||
let ident = eat_ident(stream)?;
|
||||
let colon = match stream.next() {
|
||||
Some(tt) => match &tt {
|
||||
TokenTree::Punct(punct)
|
||||
if punct.as_char() == ':' && punct.spacing() == Spacing::Alone =>
|
||||
{
|
||||
Some(tt)
|
||||
}
|
||||
_ => {
|
||||
Diagnostic::spanned(tt.span(), Level::Error, "Expected a `:`").emit();
|
||||
None
|
||||
}
|
||||
},
|
||||
None => {
|
||||
Diagnostic::new(Level::Error, "Unexpected end of stream").emit();
|
||||
None
|
||||
}
|
||||
}?;
|
||||
let expr = Expr::parse(stream)?;
|
||||
|
||||
Some(Param(ident, colon, expr))
|
||||
}
|
||||
}
|
||||
|
||||
impl Expr {
|
||||
/// ```notrust
|
||||
/// expr = <now that's just rust code, stop being a pedantic bitch about it>
|
||||
/// ```
|
||||
fn parse(stream: &mut Peekable<impl TtIter>) -> Option<Self> {
|
||||
let mut tokens = Vec::new();
|
||||
|
||||
while let Some(tt) = stream.peek() {
|
||||
if is_punct(',')(tt) {
|
||||
break;
|
||||
} else {
|
||||
tokens.push(check_ident_recurse(
|
||||
stream.next().expect("We already peeked this"),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if tokens.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Expr(tokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Listener {
|
||||
/// ```notrust
|
||||
/// listener = "@" ident space "(" space expr space ")" space
|
||||
/// ```
|
||||
fn parse(stream: &mut Peekable<impl TtIter>) -> Option<Self> {
|
||||
let at = stream.next_if(is_punct('@'))?;
|
||||
let name = eat_ident(stream)?;
|
||||
let callback = match stream.next_if(is_group(Delimiter::Parenthesis)) {
|
||||
Some(tt) => tt,
|
||||
None => {
|
||||
Diagnostic::spanned(
|
||||
at.span().join(name.span()).unwrap(),
|
||||
Level::Error,
|
||||
"Event listeners require a callback",
|
||||
)
|
||||
.emit();
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some(Self { at, name, callback })
|
||||
}
|
||||
|
||||
fn span(&self) -> Span {
|
||||
self.at.span().join(self.callback.span()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// ```notrust
|
||||
/// ident = <any valid rust identifier, excluding those starting with "__uwui">
|
||||
/// ```
|
||||
fn eat_ident(stream: &mut Peekable<impl TtIter>) -> Option<TokenTree> {
|
||||
match stream.next() {
|
||||
Some(tt) => match &tt {
|
||||
TokenTree::Ident(i) => {
|
||||
check_ident(i);
|
||||
Some(tt)
|
||||
}
|
||||
_ => {
|
||||
Diagnostic::spanned(tt.span(), Level::Error, "Expected an identifier").emit();
|
||||
None
|
||||
}
|
||||
},
|
||||
None => {
|
||||
Diagnostic::new(Level::Error, "Unexpected end of stream").emit();
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_ident_recurse(tt: TokenTree) -> TokenTree {
|
||||
match tt {
|
||||
TokenTree::Ident(ref ident) => {
|
||||
check_ident(ident);
|
||||
tt
|
||||
}
|
||||
TokenTree::Group(group) => {
|
||||
let mut wrapped = Group::new(
|
||||
group.delimiter(),
|
||||
TokenStream::from_iter(group.stream().into_iter().map(check_ident_recurse)),
|
||||
);
|
||||
wrapped.set_span(group.span());
|
||||
TokenTree::from(wrapped)
|
||||
}
|
||||
_ => tt,
|
||||
}
|
||||
}
|
||||
|
||||
fn check_ident(ident: &Ident) {
|
||||
if ident.to_string().starts_with("__uwui") {
|
||||
Diagnostic::spanned(
|
||||
ident.span(),
|
||||
Level::Error,
|
||||
"Identifiers starting with `__uwui` are reserved",
|
||||
)
|
||||
.emit();
|
||||
}
|
||||
}
|
||||
|
||||
const fn is_punct_spaced(c: char, s: Spacing) -> impl FnMut(&TokenTree) -> bool {
|
||||
move |tt| match tt {
|
||||
TokenTree::Punct(punct) => punct.as_char() == c && punct.spacing() == s,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_punct(c: char) -> impl FnMut(&TokenTree) -> bool {
|
||||
move |tt| match tt {
|
||||
TokenTree::Punct(punct) => punct.as_char() == c,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_group(delim: Delimiter) -> impl FnMut(&TokenTree) -> bool {
|
||||
move |tt| match tt {
|
||||
TokenTree::Group(group) => group.delimiter() == delim,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
///////////// debug stuff ///////////////
|
||||
|
||||
impl Component {
|
||||
fn print(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
|
||||
let indent = " ".repeat(depth);
|
||||
|
||||
f.write_str(&indent)?;
|
||||
fmt::Debug::fmt(&self.name, f)?;
|
||||
if let Some(params) = &self.params {
|
||||
f.write_str("(")?;
|
||||
let mut first = true;
|
||||
for param in params {
|
||||
if first {
|
||||
first = false;
|
||||
} else {
|
||||
f.write_str(", ")?;
|
||||
}
|
||||
fmt::Debug::fmt(param, f)?;
|
||||
}
|
||||
f.write_str(")")?;
|
||||
}
|
||||
f.write_str("\n")?;
|
||||
|
||||
if let Some(children) = &self.children {
|
||||
for child in children {
|
||||
child.print(f, depth + 1)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Component {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str("\n")?;
|
||||
self.print(f, 0)?;
|
||||
f.write_str("\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Name {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
for segment in &self.0 {
|
||||
f.write_str(&segment.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Param {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(&self.0.to_string())?;
|
||||
f.write_str(" => ")?;
|
||||
fmt::Debug::fmt(&self.2, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Expr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
for tt in &self.0 {
|
||||
f.write_str(&tt.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue