diff --git a/compiler-core/src/api.rs b/compiler-core/src/api.rs index 42f5af91..b5aaad40 100644 --- a/compiler-core/src/api.rs +++ b/compiler-core/src/api.rs @@ -8,7 +8,7 @@ use crate::comp::{CompDoc, Compiler, CompilerError}; use crate::lang::Preset; use crate::metrics::CompilerMetrics; use crate::pack::{self, PackedProject, PackerError, PackerResult, Resource, ValidUse}; -use crate::plug::run_plugins; +use crate::plug::{run_plugins, PluginRuntime}; /// Output of the compiler API #[derive(Debug, Clone)] @@ -35,6 +35,7 @@ pub struct OkOutput { #[derive(Default, Debug, Clone)] pub struct CompilerMetadata { pub presets: HashMap, + pub plugins: Vec, pub default_icon_priority: i64, } @@ -96,7 +97,7 @@ pub async fn compile(root_resource: &Resource, setting: &Setting) -> CompilerOut let ms = metrics.comp_done(); info!("comp phase done in {ms}ms"); - let comp_doc = run_plugins(comp_doc); + let comp_doc = run_plugins(comp_doc, &comp_meta.plugins).await; let ms = metrics.plug_done(); info!("plug phase done in {ms}ms"); diff --git a/compiler-core/src/comp/comp_doc.rs b/compiler-core/src/comp/comp_doc.rs index 237d2ea2..7bbce5ee 100644 --- a/compiler-core/src/comp/comp_doc.rs +++ b/compiler-core/src/comp/comp_doc.rs @@ -1,4 +1,4 @@ -use celerctypes::{DocPoorText, RouteMetadata}; +use celerctypes::{DocDiagnostic, DocPoorText, RouteMetadata}; use serde::{Deserialize, Serialize}; use crate::api::CompilerMetadata; @@ -21,6 +21,8 @@ pub struct CompDoc { pub preface: Vec>, /// The route pub route: Vec, + /// Overall diagnostics (that don't apply to any line) + pub diagnostics: Vec, } impl Compiler { @@ -50,6 +52,7 @@ impl Compiler { project: self.project, preface, route: route_vec, + diagnostics: vec![], }, self.meta, )) @@ -118,6 +121,7 @@ impl Compiler { route: vec![self.create_empty_section_for_error(errors).await], project: self.project, preface: vec![], + diagnostics: vec![], }, self.meta, ) diff --git a/compiler-core/src/comp/compiler_builder.rs b/compiler-core/src/comp/compiler_builder.rs index c8c67ddb..9420e95d 100644 --- a/compiler-core/src/comp/compiler_builder.rs +++ b/compiler-core/src/comp/compiler_builder.rs @@ -43,6 +43,7 @@ impl CompilerBuilder { meta: CompilerMetadata { presets: self.presets, default_icon_priority: self.default_icon_priority, + ..Default::default() }, color: self.color, coord: self.coord, diff --git a/compiler-core/src/comp/prop.rs b/compiler-core/src/comp/prop.rs index 5a02fde5..875f0831 100644 --- a/compiler-core/src/comp/prop.rs +++ b/compiler-core/src/comp/prop.rs @@ -28,6 +28,7 @@ pub const NAME: &str = "name"; pub const NOTES: &str = "notes"; pub const PRESETS: &str = "presets"; pub const PRIORITY: &str = "priority"; +pub const PLUGINS: &str = "plugins"; pub const ROUTE: &str = "route"; pub const SCALE: &str = "scale"; pub const SIZE: &str = "size"; @@ -43,4 +44,5 @@ pub const TRANSLATE: &str = "translate"; pub const USE: &str = "use"; pub const VERSION: &str = "version"; pub const WARP: &str = "warp"; +pub const WITH: &str = "with"; pub const ZOOM_BOUNDS: &str = "zoom-bounds"; diff --git a/compiler-core/src/exec/exec_doc.rs b/compiler-core/src/exec/exec_doc.rs index be2f62e6..719e8f1d 100644 --- a/compiler-core/src/exec/exec_doc.rs +++ b/compiler-core/src/exec/exec_doc.rs @@ -19,6 +19,7 @@ impl CompDoc { project: self.project, preface: self.preface, route: sections, + diagnostics: self.diagnostics, }) } } @@ -26,11 +27,12 @@ impl CompDoc { #[cfg(test)] mod test { use celerctypes::{ - DocPoorText, ExecLine, ExecMapSection, ExecSection, GameCoord, MapLine, MapMetadata, - RouteMetadata, + DocDiagnostic, DocPoorText, ExecLine, ExecMapSection, ExecSection, GameCoord, MapLine, + MapMetadata, RouteMetadata, }; use crate::comp::{CompLine, CompMovement, CompSection}; + use crate::lang::parse_poor; use super::*; @@ -44,15 +46,23 @@ mod test { let test_preface = vec![vec![DocPoorText::Text("test".to_string())]]; + let test_diagnostics = vec![DocDiagnostic { + msg: parse_poor("test msg"), + msg_type: "test".to_string(), + source: "test".to_string(), + }]; + let test_doc = CompDoc { project: test_metadata.clone(), preface: test_preface.clone(), + diagnostics: test_diagnostics.clone(), ..Default::default() }; let exec_doc = test_doc.exec().await.unwrap(); assert_eq!(exec_doc.project, test_metadata); assert_eq!(exec_doc.preface, test_preface); + assert_eq!(exec_doc.diagnostics, test_diagnostics); } #[tokio::test] diff --git a/compiler-core/src/lang/rich/parse.rs b/compiler-core/src/lang/rich/parse.rs index 24aaf0ce..00bf3897 100644 --- a/compiler-core/src/lang/rich/parse.rs +++ b/compiler-core/src/lang/rich/parse.rs @@ -91,7 +91,7 @@ fn parse_tagexp(pt: &pt::TagExp) -> DocRichText { arg.push_str(str); } for pt_unit in pt.m_arg.iter() { - append_unit_to_string(pt_unit, &mut arg); + append_unit_inside_tag_to_string(pt_unit, &mut arg); } DocRichText { tag: Some(tag), @@ -99,6 +99,19 @@ fn parse_tagexp(pt: &pt::TagExp) -> DocRichText { link: None, } } +fn append_unit_inside_tag_to_string(pt: &pt::UnitInsideTag, out: &mut String) { + match pt { + pt::UnitInsideTag::Unit(pt) => { + append_unit_to_string(pt, out); + } + pt::UnitInsideTag::UnitDotSymbol(_) => { + out.push('.'); + } + pt::UnitInsideTag::UnitOpenParenSymbol(_) => { + out.push('('); + } + } +} fn append_unit_to_string(pt: &pt::Unit, out: &mut String) { match pt { @@ -253,4 +266,28 @@ mod test { }] ); } + + #[test] + fn test_dot_in_text() { + assert_eq!( + parse_rich(".tag([hello]continue.me)"), + vec![DocRichText { + tag: Some("tag".to_string()), + text: "[hello]continue.me".to_string(), + link: None + }] + ); + } + + #[test] + fn test_open_paren_in_text() { + assert_eq!( + parse_rich(".tag([hello]co(ntinue.me)"), + vec![DocRichText { + tag: Some("tag".to_string()), + text: "[hello]co(ntinue.me".to_string(), + link: None + }] + ); + } } diff --git a/compiler-core/src/lang/rich/rich.grammar b/compiler-core/src/lang/rich/rich.grammar index e49f8d21..0c7127aa 100644 --- a/compiler-core/src/lang/rich/rich.grammar +++ b/compiler-core/src/lang/rich/rich.grammar @@ -21,12 +21,15 @@ rule TagExp( (Tag) tag: token Identifier, _: token Symbol"(", space: optional token Space, - (Arg) arg: optional Unit+, + (Arg) arg: optional UnitInsideTag+, _: token Symbol")" ); rule Unit = UnitId | UnitEscape; +rule UnitInsideTag = Unit | UnitDotSymbol | UnitOpenParenSymbol; rule UnitId(t: token Identifier, s: optional token Space); rule UnitEscape(t: token Escape, s: optional token Space); +rule UnitDotSymbol((Text)_: token Symbol".", s: optional token Space); +rule UnitOpenParenSymbol((Text)_: token Symbol"(", s: optional token Space); rule Symbol((Text)t: token Symbol); rule Space(t: token Space); diff --git a/compiler-core/src/pack/mod.rs b/compiler-core/src/pack/mod.rs index b593f978..50e55729 100644 --- a/compiler-core/src/pack/mod.rs +++ b/compiler-core/src/pack/mod.rs @@ -7,6 +7,7 @@ //! The output of the packer is a [`RouteMetadata`](celerctypes::RouteMetadata) //! and a json blob of the route. +use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use std::collections::BTreeMap; use std::convert::Infallible; @@ -33,10 +34,9 @@ pub use resource::*; use crate::json::Cast; use crate::lang::parse_poor; -#[cfg(feature = "wasm")] -use crate::util::WasmError; -#[derive(Debug, Clone, PartialEq, thiserror::Error)] +#[derive(Debug, Clone, PartialEq, thiserror::Error, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", tag = "type", content = "data")] pub enum PackerError { #[error("The project file (project.yaml) is missing or invalid.")] InvalidProject, @@ -71,6 +71,9 @@ pub enum PackerError { #[error("Error when parsing structured data in file {0}: {1}")] InvalidFormat(String, String), + #[error("Error when parsing file {0}: file is not UTF-8")] + InvalidUtf8(String), + #[error("")] InvalidIcon, @@ -106,6 +109,9 @@ pub enum PackerError { )] DuplicateMap(usize), + #[error("`{0}` is not a valid built-in plugin or reference to a plugin script")] + InvalidPlugin(String), + #[error("No map defined in project config")] MissingMap, @@ -114,10 +120,6 @@ pub enum PackerError { #[error("{0}")] NotImpl(String), - - #[cfg(feature = "wasm")] - #[error("Wasm execution error: {0}")] - Wasm(#[from] WasmError), } impl PackerError { @@ -130,11 +132,7 @@ impl PackerError { } pub fn is_cancel(&self) -> bool { - #[cfg(feature = "wasm")] - let x = matches!(self, Self::Wasm(WasmError::Cancel)); - #[cfg(not(feature = "wasm"))] - let x = false; - x + false } } @@ -150,7 +148,8 @@ pub type PackerResult = Result; /// /// This is used to expose errors to the compiler, so it can be displayed /// using the diagnostics API -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] pub enum PackerValue { Ok(Value), Err(PackerError), diff --git a/compiler-core/src/pack/pack_config.rs b/compiler-core/src/pack/pack_config.rs index 969fac30..bc692eae 100644 --- a/compiler-core/src/pack/pack_config.rs +++ b/compiler-core/src/pack/pack_config.rs @@ -1,12 +1,13 @@ use std::collections::HashMap; use celerctypes::{DocTag, MapMetadata}; -use serde_json::{Map, Value}; +use serde_json::{json, Value}; use crate::api::Setting; use crate::comp::prop; use crate::json::{Cast, Coerce}; use crate::lang::Preset; +use crate::plug::{BuiltInPlugin, Plugin, PluginRuntime}; use crate::util::async_for; use super::{pack_map, pack_presets, PackerError, PackerResult, Resource, Use, ValidUse}; @@ -17,6 +18,7 @@ pub struct ConfigBuilder { pub icons: HashMap, pub tags: HashMap, pub presets: HashMap, + pub plugins: Vec, pub default_icon_priority: Option, } @@ -28,18 +30,48 @@ pub async fn pack_config( index: usize, setting: &Setting, ) -> PackerResult<()> { - // Load and resolve top-level `use` properties - let config_value = match Use::from(config) { - Use::Invalid(path) => return Err(PackerError::InvalidUse(path)), - Use::NotUse(v) => v, - Use::Valid(valid_use) => load_config_from_use(project_resource, valid_use, index).await?, - }; + match Use::try_from(config) { + Ok(Use::Invalid(path)) => Err(PackerError::InvalidUse(path)), + Ok(Use::Valid(valid_use)) => { + // load a config from top-level use object + process_config_from_use(builder, project_resource, valid_use, index, setting).await + } + Err(v) => { + // load a config directly from the object + process_config(builder, project_resource, v, index, setting).await + } + } +} - // Resolve `use`s inside the properties - let config_value = process_config(project_resource, config_value, index).await?; +/// Load a top-level `use` +async fn process_config_from_use( + builder: &mut ConfigBuilder, + project_resource: &Resource, + use_prop: ValidUse, + index: usize, + setting: &Setting, +) -> PackerResult<()> { + let config_resource = project_resource.resolve(&use_prop).await?; + let config = config_resource.load_structured().await?; + // process this config with the config resource context instead of the project context + // so `use`'s inside are resolved correctly + process_config(builder, &config_resource, config, index, setting).await +} + +/// Process a config, adding values to Builder and use the resource to resolve `use`'s +async fn process_config( + builder: &mut ConfigBuilder, + resource: &Resource, + config: Value, + index: usize, + setting: &Setting, +) -> PackerResult<()> { + let config = config + .try_into_object() + .map_err(|_| PackerError::InvalidConfigType(index))?; // add values to builder - async_for!((key, value) in config_value.into_iter(), { + let _ = async_for!((key, value) in config.into_iter(), { match key.as_ref() { prop::MAP => { if builder.map.is_some() { @@ -48,90 +80,122 @@ pub async fn pack_config( builder.map = Some(pack_map(value, index).await?); } prop::ICONS => { - let icons = value.try_into_object().map_err(|_| PackerError::InvalidConfigProperty(index, prop::ICONS.to_string()))?; - async_for!((key, value) in icons.into_iter(), { - if value.is_array() || value.is_object() { - return Err(PackerError::InvalidConfigProperty(index, format!("{}.{}", prop::ICONS, key))); - } - builder.icons.insert(key, value.coerce_to_string()); - })?; + process_icons_config(builder, resource, value, index).await?; } prop::TAGS => { let tags = value.try_into_object().map_err(|_| PackerError::InvalidConfigProperty(index, prop::TAGS.to_string()))?; - async_for!((key, value) in tags.into_iter(), { + let _ = async_for!((key, value) in tags.into_iter(), { let tag = serde_json::from_value::(value).map_err(|_| PackerError::InvalidConfigProperty(index, format!("{}.{}", prop::TAGS, key)))?; builder.tags.insert(key, tag); - })?; + }); } prop::PRESETS => { let presets = pack_presets(value, index, setting.max_preset_namespace_depth).await?; - async_for!((key, value) in presets.into_iter(), { + let _ = async_for!((key, value) in presets.into_iter(), { builder.presets.insert(key, value); - })?; + }); } prop::DEFAULT_ICON_PRIORITY => { let priority = value.try_coerce_to_i64().ok_or_else(|| PackerError::InvalidConfigProperty(index, prop::DEFAULT_ICON_PRIORITY.to_string()))?; builder.default_icon_priority = Some(priority); } + prop::PLUGINS => { + process_plugins_config(builder, resource, value, index).await?; + } _ => return Err(PackerError::UnusedConfigProperty(index, key)), } - })?; + }); Ok(()) } -/// Load a top-level `use` -async fn load_config_from_use( - project_resource: &Resource, - use_prop: ValidUse, - index: usize, -) -> PackerResult { - let config_resource = project_resource.resolve(&use_prop).await?; - let config = config_resource.load_structured().await?; - // Calling process_config here - // because any `use` inside the config needs to be resolved by the config resource - // not the project resource - let config = process_config(&config_resource, config, index).await?; - Ok(Value::Object(config)) -} - -/// Process a config and resolve all `use`s inside -async fn process_config( +/// Process the `icons` property +/// +/// Resolves `use`'s using the resource context and add the icon URLs to the builder +async fn process_icons_config( + builder: &mut ConfigBuilder, resource: &Resource, - config: Value, + icons: Value, index: usize, -) -> PackerResult> { - let mut config_obj = match config { - Value::Object(obj) => obj, - _ => return Err(PackerError::InvalidConfigType(index)), - }; - - let icons = match config_obj.get_mut(prop::ICONS) { - Some(v) => v, - None => return Ok(config_obj), - }; - - let icons = match icons.as_object_mut() { - Some(obj) => obj, - // just returning ok here - // the error will be caught later - _ => return Ok(config_obj), - }; +) -> PackerResult<()> { + let icons = icons + .try_into_object() + .map_err(|_| PackerError::InvalidConfigProperty(index, prop::ICONS.to_string()))?; - async_for!(value in icons.values_mut(), { - let v = value.take(); - match Use::from(v) { - Use::Invalid(path) => return Err(PackerError::InvalidUse(path)), - Use::NotUse(v) => { - *value = v; + let _ = async_for!((key, v) in icons.into_iter(), { + match Use::try_from(v) { + Err(v) => { + // not a use, just a icon url + if v.is_array() || v.is_object() { + return Err(PackerError::InvalidConfigProperty(index, format!("{}.{}", prop::ICONS, key))); + } + builder.icons.insert(key, v.coerce_to_string()); } - Use::Valid(valid_use) => { + Ok(Use::Invalid(path)) => return Err(PackerError::InvalidUse(path)), + Ok(Use::Valid(valid_use)) => { let icon_resource = resource.resolve(&valid_use).await?; let image_url = icon_resource.load_image_url().await?; - *value = Value::String(image_url); + builder.icons.insert(key, image_url); } } - })?; + }); - Ok(config_obj) + Ok(()) +} + +/// Process the `plugins` property +/// +/// Resolves `use`'s using the resource context and add the plugins to the builder +async fn process_plugins_config( + builder: &mut ConfigBuilder, + resource: &Resource, + plugins: Value, + index: usize, +) -> PackerResult<()> { + let plugins = plugins + .try_into_array() + .map_err(|_| PackerError::InvalidConfigProperty(index, prop::PLUGINS.to_string()))?; + + let _ = async_for!((i, v) in plugins.into_iter().enumerate(), { + let v = v.try_into_object().map_err(|_| PackerError::InvalidConfigProperty(index, format!("{}[{}]", prop::PLUGINS, i)))?; + let mut plugin = None; + let mut props = json!(null); + let _ = async_for!((key, value) in v.into_iter(), { + match key.as_ref() { + prop::USE => { + let use_path_string = value.coerce_to_string(); + plugin = match serde_json::from_value::(value) { + Ok(built_in) => Some(Plugin::BuiltIn(built_in)), + Err(e) => { + log::info!("e: {e}"); + // it's a script path, parse as use + match Use::from(use_path_string) { + Use::Invalid(path) => return Err(PackerError::InvalidPlugin(path)), + Use::Valid(valid_use) => { + // load the script + let script_resource = resource.resolve(&valid_use).await?; + let script = script_resource.load_utf8().await?; + Some(Plugin::Script(script)) + } + } + } + }; + } + prop::WITH => { + props = value; + } + _ => return Err(PackerError::UnusedConfigProperty(index, format!("{}[{}].{}", prop::PLUGINS, i, key))), + } + }); + let plugin = match plugin { + Some(v) => v, + None => return Err(PackerError::MissingConfigProperty(index, format!("{}[{}].{}", prop::PLUGINS, i, prop::USE))), + }; + builder.plugins.push(PluginRuntime { + plugin, + props, + }); + }); + + Ok(()) } diff --git a/compiler-core/src/pack/pack_map.rs b/compiler-core/src/pack/pack_map.rs index b8494e54..269419fc 100644 --- a/compiler-core/src/pack/pack_map.rs +++ b/compiler-core/src/pack/pack_map.rs @@ -89,9 +89,9 @@ pub async fn pack_map(value: Value, index: usize) -> PackerResult { let layers = { let mut packed_layers = Vec::with_capacity(layers.len()); - async_for!((i, layer) in layers.into_iter().enumerate(), { + let _ = async_for!((i, layer) in layers.into_iter().enumerate(), { packed_layers.push(pack_map_layer(layer, index, i)?); - })?; + }); packed_layers }; diff --git a/compiler-core/src/pack/pack_preset.rs b/compiler-core/src/pack/pack_preset.rs index 73544bca..3389e34d 100644 --- a/compiler-core/src/pack/pack_preset.rs +++ b/compiler-core/src/pack/pack_preset.rs @@ -42,7 +42,7 @@ async fn pack_presets_internal( } })?; - async_for!((key, value) in obj.into_iter(), { + let _ = async_for!((key, value) in obj.into_iter(), { if let Some(namespace) = key.strip_prefix('_') { // sub namespace let full_key = if preset_name.is_empty() { @@ -63,7 +63,7 @@ async fn pack_presets_internal( })?; output.push((full_key, preset)); } - })?; + }); Ok(()) } diff --git a/compiler-core/src/pack/pack_project.rs b/compiler-core/src/pack/pack_project.rs index 7493d3ef..5dba8292 100644 --- a/compiler-core/src/pack/pack_project.rs +++ b/compiler-core/src/pack/pack_project.rs @@ -71,9 +71,9 @@ pub async fn pack_project( .map_err(|_| PackerError::InvalidMetadataPropertyType(prop::CONFIG.to_string()))?; let mut builder = ConfigBuilder::default(); - async_for!((i, config) in config.into_iter().enumerate(), { + let _ = async_for!((i, config) in config.into_iter().enumerate(), { pack_config(&mut builder, project_resource, config, i, setting).await?; - })?; + }); let route_metadata = RouteMetadata { name: project_resource.name().to_string(), @@ -86,6 +86,7 @@ pub async fn pack_project( let compiler_metadata = CompilerMetadata { presets: builder.presets, + plugins: builder.plugins, default_icon_priority: builder.default_icon_priority.unwrap_or(2), }; diff --git a/compiler-core/src/pack/pack_route.rs b/compiler-core/src/pack/pack_route.rs index 42c5fab1..f4dc4ced 100644 --- a/compiler-core/src/pack/pack_route.rs +++ b/compiler-core/src/pack/pack_route.rs @@ -51,11 +51,11 @@ async fn pack_route_internal( Ok(arr) => { let mut output = vec![]; let _ = async_for!(x in arr.into_iter(), { - match Use::from(x) { - Use::Invalid(path) => { + match Use::try_from(x) { + Ok(Use::Invalid(path)) => { output.push(PackerValue::Err(PackerError::InvalidUse(path))); } - Use::NotUse(x) => { + Err(x) => { let result = pack_route_internal( resource, x, @@ -66,7 +66,7 @@ async fn pack_route_internal( ).await; output.push(result); } - Use::Valid(valid_use) => { + Ok(Use::Valid(valid_use)) => { let result = resolve_use( resource, valid_use, @@ -91,9 +91,9 @@ async fn pack_route_internal( Err(route) => route, }; - match Use::from(route) { - Use::Invalid(path) => PackerValue::Err(PackerError::InvalidUse(path)), - Use::NotUse(x) => { + match Use::try_from(route) { + Ok(Use::Invalid(path)) => PackerValue::Err(PackerError::InvalidUse(path)), + Err(x) => { // array case is covered above match x.try_into_object() { Ok(obj) => { @@ -118,7 +118,7 @@ async fn pack_route_internal( } } } - Use::Valid(valid_use) => { + Ok(Use::Valid(valid_use)) => { resolve_use(resource, valid_use, use_depth, max_use_depth, max_ref_depth).await } } diff --git a/compiler-core/src/pack/pack_use.rs b/compiler-core/src/pack/pack_use.rs index 279da642..dda64ea2 100644 --- a/compiler-core/src/pack/pack_use.rs +++ b/compiler-core/src/pack/pack_use.rs @@ -11,8 +11,6 @@ use crate::json::Coerce; pub enum Use { /// Correctly formed `use` property Valid(ValidUse), - /// Not loading a resource - NotUse(Value), /// Invalid path specified in the use property Invalid(String), } @@ -33,24 +31,11 @@ pub enum ValidUse { }, } -impl From for Use { - fn from(value: Value) -> Self { - let obj = match value.as_object() { - Some(obj) => obj, - None => return Self::NotUse(value), - }; - let mut iter = obj.iter(); - let (key, v) = match iter.next() { - Some((key, value)) => (key, value), - None => return Self::NotUse(value), - }; - if iter.next().is_some() { - return Self::NotUse(value); - } - if key != prop::USE { - return Self::NotUse(value); - } - let v = v.coerce_to_string(); +impl From for Use { + /// Convert a path in the `use` property to a Use object + /// + /// If the path is malformed, this returns a [`Use::Invalid`] + fn from(v: String) -> Self { if v.starts_with('/') { if v.ends_with('/') { Self::Invalid(v) @@ -95,6 +80,36 @@ impl From for Use { } } +impl TryFrom for Use { + type Error = Value; + + /// Try converting a json object in the form of `{ "use": "..." }` to a `Use` + /// + /// If the object is not in the correct form, the original value is returned. + /// This includes if the object has keys other than `use`. + /// + /// If the object is in the correct form but the path is not, this returns an Ok variant with + /// [`Use::Invalid`] + fn try_from(value: Value) -> Result { + let obj = match value.as_object() { + Some(obj) => obj, + None => return Err(value), + }; + let mut iter = obj.iter(); + let (key, v) = match iter.next() { + Some((key, value)) => (key, value), + None => return Err(value), + }; + if iter.next().is_some() { + return Err(value); + } + if key != prop::USE { + return Err(value); + } + Ok(Self::from(v.coerce_to_string())) + } +} + #[cfg(test)] mod test { use serde_json::json; @@ -114,7 +129,7 @@ mod test { ]; for test in tests { - assert_eq!(Use::from(test.clone()), Use::NotUse(test)); + assert_eq!(Use::try_from(test.clone()), Err(test)); } } @@ -132,104 +147,104 @@ mod test { ]; for test in tests { - assert_eq!(Use::from(test.clone()), Use::NotUse(test)); + assert_eq!(Use::try_from(test.clone()), Err(test)); } } #[test] fn test_use_relative() { assert_eq!( - Use::from(json!({ + Use::try_from(json!({ "use": "./hello" })), - Use::Valid(ValidUse::Relative("./hello".to_string())) + Ok(Use::Valid(ValidUse::Relative("./hello".to_string()))) ); assert_eq!( - Use::from(json!({ + Use::try_from(json!({ "use": "../foo/hello" })), - Use::Valid(ValidUse::Relative("../foo/hello".to_string())) + Ok(Use::Valid(ValidUse::Relative("../foo/hello".to_string()))) ); } #[test] fn test_use_absolute() { assert_eq!( - Use::from(json!({ + Use::try_from(json!({ "use": "/hello" })), - Use::Valid(ValidUse::Absolute("/hello".to_string())) + Ok(Use::Valid(ValidUse::Absolute("/hello".to_string()))) ); assert_eq!( - Use::from(json!({ + Use::try_from(json!({ "use": "/foo/hello" })), - Use::Valid(ValidUse::Absolute("/foo/hello".to_string())) + Ok(Use::Valid(ValidUse::Absolute("/foo/hello".to_string()))) ); assert_eq!( - Use::from(json!({ + Use::try_from(json!({ "use": "//foo/hello" })), - Use::Valid(ValidUse::Absolute("//foo/hello".to_string())) + Ok(Use::Valid(ValidUse::Absolute("//foo/hello".to_string()))) ); } #[test] fn test_use_remote() { assert_eq!( - Use::from(json!({ + Use::try_from(json!({ "use": "foo/hello/bar" })), - Use::Valid(ValidUse::Remote { + Ok(Use::Valid(ValidUse::Remote { owner: "foo".to_string(), repo: "hello".to_string(), path: "bar".to_string(), reference: None, - }) + })) ); assert_eq!( - Use::from(json!({ + Use::try_from(json!({ "use": "foo/hello/bar:test" })), - Use::Valid(ValidUse::Remote { + Ok(Use::Valid(ValidUse::Remote { owner: "foo".to_string(), repo: "hello".to_string(), path: "bar".to_string(), reference: Some("test".to_string()), - }) + })) ); assert_eq!( - Use::from(json!({ + Use::try_from(json!({ "use": ".foo/hello/bar/giz" })), - Use::Valid(ValidUse::Remote { + Ok(Use::Valid(ValidUse::Remote { owner: ".foo".to_string(), repo: "hello".to_string(), path: "bar/giz".to_string(), reference: None, - }) + })) ); assert_eq!( - Use::from(json!({ + Use::try_from(json!({ "use": "foo/hello/bar/giz/biz:test" })), - Use::Valid(ValidUse::Remote { + Ok(Use::Valid(ValidUse::Remote { owner: "foo".to_string(), repo: "hello".to_string(), path: "bar/giz/biz".to_string(), reference: Some("test".to_string()), - }) + })) ); assert_eq!( - Use::from(json!({ + Use::try_from(json!({ "use": "foo/hello/bar/giz/biz:" })), - Use::Valid(ValidUse::Remote { + Ok(Use::Valid(ValidUse::Remote { owner: "foo".to_string(), repo: "hello".to_string(), path: "bar/giz/biz".to_string(), reference: None, - }) + })) ); } @@ -255,7 +270,10 @@ mod test { ]; for test in tests { - assert_eq!(Use::from(make_use(test)), Use::Invalid(test.to_string())); + assert_eq!( + Use::try_from(make_use(test)), + Ok(Use::Invalid(test.to_string())) + ); } } } diff --git a/compiler-core/src/pack/resource/loader.rs b/compiler-core/src/pack/resource/loader.rs index 5a531360..d93af3e0 100644 --- a/compiler-core/src/pack/resource/loader.rs +++ b/compiler-core/src/pack/resource/loader.rs @@ -9,6 +9,14 @@ pub trait ResourceLoader { /// Load a resource as raw bytes async fn load_raw(&self, path: &str) -> PackerResult>; + async fn load_utf8(&self, path: &str) -> PackerResult { + let bytes = self.load_raw(path).await?; + match String::from_utf8(bytes) { + Ok(v) => Ok(v), + Err(_) => Err(PackerError::InvalidUtf8(path.to_string())), + } + } + /// Load structured value. The type depends on the file extension read from the ref async fn load_structured(&self, path: &str) -> PackerResult { let v = if path.ends_with(".yaml") || path.ends_with(".yml") { diff --git a/compiler-core/src/pack/resource/resource_impl.rs b/compiler-core/src/pack/resource/resource_impl.rs index c0146bef..2255f5aa 100644 --- a/compiler-core/src/pack/resource/resource_impl.rs +++ b/compiler-core/src/pack/resource/resource_impl.rs @@ -73,6 +73,7 @@ impl Resource { } loader_delegate!(load_structured, Value); + loader_delegate!(load_utf8, String); loader_delegate!(load_image_url, String); pub async fn resolve(&self, target: &ValidUse) -> PackerResult { diff --git a/compiler-core/src/plug/link.rs b/compiler-core/src/plug/link.rs new file mode 100644 index 00000000..aeb8871d --- /dev/null +++ b/compiler-core/src/plug/link.rs @@ -0,0 +1,172 @@ +//! Link plugin +//! +//! This plugin looks for the `link` tag and transforms it into a link. + +use celerctypes::DocRichText; + +use super::operation; +use crate::comp::{prop, CompDoc}; + +pub async fn run_link_plugin(comp_doc: &mut CompDoc) { + // add the link tag if not defined already + comp_doc + .project + .tags + .entry(prop::LINK.to_string()) + .or_default(); + operation::for_all_lines(comp_doc, |mut line| async { + operation::for_all_rich_text(&mut line, transform_link_tag).await; + line + }) + .await +} + +fn transform_link_tag(rich_text: &mut DocRichText) { + if rich_text + .tag + .as_ref() + .filter(|tag| tag == &prop::LINK) + .is_none() + { + return; + } + if rich_text.link.is_some() { + return; + } + + if rich_text.text.starts_with('[') { + match rich_text.text.find(']') { + Some(i) => { + rich_text.link = Some(rich_text.text[i + 1..].trim().to_string()); + rich_text.text = rich_text.text[1..i].to_string(); + } + None => { + rich_text.link = Some(rich_text.text.trim().to_string()); + } + } + } else { + rich_text.link = Some(rich_text.text.trim().to_string()); + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_ignore_no_tag() { + let mut text = DocRichText::text("hello world"); + let expected = text.clone(); + transform_link_tag(&mut text); + assert_eq!(expected, text); + } + + #[test] + fn test_ignore_link() { + let mut text = DocRichText { + tag: Some("link".to_string()), + text: "hello world".to_string(), + link: Some("https://example.com".to_string()), + }; + let expected = text.clone(); + transform_link_tag(&mut text); + assert_eq!(expected, text); + } + + #[test] + fn test_ignore_non_link_tag() { + let mut text = DocRichText { + tag: Some("test".to_string()), + text: "hello world".to_string(), + link: None, + }; + let expected = text.clone(); + transform_link_tag(&mut text); + assert_eq!(expected, text); + } + + #[test] + fn test_transform_link_tag() { + let mut text = DocRichText { + tag: Some(prop::LINK.to_string()), + text: "hello world".to_string(), + link: None, + }; + transform_link_tag(&mut text); + assert_eq!( + text, + DocRichText { + tag: Some(prop::LINK.to_string()), + text: "hello world".to_string(), + link: Some("hello world".to_string()), + } + ); + } + + #[test] + fn test_transform_link_tag_with_text() { + let mut text = DocRichText { + tag: Some(prop::LINK.to_string()), + text: "[hello world] i am link".to_string(), + link: None, + }; + transform_link_tag(&mut text); + assert_eq!( + text, + DocRichText { + tag: Some(prop::LINK.to_string()), + text: "hello world".to_string(), + // link should be trimmed + link: Some("i am link".to_string()), + } + ); + } + + #[test] + fn test_transform_partial_bracket() { + let mut text = DocRichText { + tag: Some(prop::LINK.to_string()), + text: "[hello world i am link".to_string(), + link: None, + }; + transform_link_tag(&mut text); + assert_eq!( + text, + DocRichText { + tag: Some(prop::LINK.to_string()), + text: "[hello world i am link".to_string(), + link: Some("[hello world i am link".to_string()), + } + ); + + let mut text = DocRichText { + tag: Some(prop::LINK.to_string()), + text: "abc[hello world] i am link".to_string(), + link: None, + }; + transform_link_tag(&mut text); + assert_eq!( + text, + DocRichText { + tag: Some(prop::LINK.to_string()), + text: "abc[hello world] i am link".to_string(), + link: Some("abc[hello world] i am link".to_string()), + } + ); + + let mut text = DocRichText { + tag: Some(prop::LINK.to_string()), + text: "abchello world] i am link".to_string(), + link: None, + }; + transform_link_tag(&mut text); + assert_eq!( + text, + DocRichText { + tag: Some(prop::LINK.to_string()), + text: "abchello world] i am link".to_string(), + link: Some("abchello world] i am link".to_string()), + } + ); + } +} diff --git a/compiler-core/src/plug/mod.rs b/compiler-core/src/plug/mod.rs index 0abe0494..9ac07ec4 100644 --- a/compiler-core/src/plug/mod.rs +++ b/compiler-core/src/plug/mod.rs @@ -1,6 +1,85 @@ +use celerctypes::DocDiagnostic; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + use crate::comp::CompDoc; +use crate::lang::parse_poor; + +mod link; +mod operation; + +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum PlugError { + #[error("{0}")] + NotImpl(String), +} +impl PlugError { + pub fn add_to_diagnostics(&self, output: &mut Vec) { + output.push(DocDiagnostic { + msg: parse_poor(&self.to_string()), + msg_type: "error".to_string(), + // TODO #24 get plugin name dynamically + source: "celerc/plugin".to_string(), + }); + } +} + +pub type PlugResult = Result; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PluginRuntime { + pub plugin: Plugin, + pub props: Value, +} + +impl PluginRuntime { + pub async fn run(&self, comp_doc: &mut CompDoc) -> PlugResult<()> { + match &self.plugin { + Plugin::BuiltIn(built_in) => built_in.run(comp_doc).await, + Plugin::Script(_) => Err(PlugError::NotImpl( + "Script plugins are not implemented yet".to_string(), + )), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum Plugin { + BuiltIn(BuiltInPlugin), + Script(String), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum BuiltInPlugin { + /// Transform link tags to clickable links. See [`link`] + Link, +} + +impl BuiltInPlugin { + pub async fn run(&self, comp_doc: &mut CompDoc) -> PlugResult<()> { + match self { + BuiltInPlugin::Link => { + link::run_link_plugin(comp_doc).await; + Ok(()) + } + } + } +} -pub fn run_plugins(comp_doc: CompDoc) -> CompDoc { - // currently just a pass-through +pub async fn run_plugins(mut comp_doc: CompDoc, plugins: &[PluginRuntime]) -> CompDoc { + let mut errors = Vec::new(); + for plugin in plugins { + if let Err(e) = plugin.run(&mut comp_doc).await { + errors.push(e); + } + } + if !errors.is_empty() { + for error in errors { + error.add_to_diagnostics(&mut comp_doc.diagnostics); + } + } comp_doc } diff --git a/compiler-core/src/plug/operation.rs b/compiler-core/src/plug/operation.rs new file mode 100644 index 00000000..cf2ac09c --- /dev/null +++ b/compiler-core/src/plug/operation.rs @@ -0,0 +1,44 @@ +use celerctypes::DocRichText; +use futures::Future; + +use crate::{ + comp::{CompDoc, CompLine}, + util::async_for, +}; + +/// Transform all [`CompLine`] in a document with function F +pub async fn for_all_lines(comp_doc: &mut CompDoc, fun: Func) +where + Func: Fn(CompLine) -> Fut, + Fut: Future, +{ + // TODO #78: async_for no longer needed in the future + let _ = async_for!(section in comp_doc.route.iter_mut(), { + let lines = std::mem::take(&mut section.lines); + let _ = async_for!(line in lines.into_iter(), { + section.lines.push(fun(line).await); + }); + }); +} + +/// Transform all rich text in a line with function F +pub async fn for_all_rich_text(comp_line: &mut CompLine, fun: F) +where + F: Fn(&mut DocRichText), +{ + // TODO #78: async_for no longer needed in the future + let _ = async_for!(t in comp_line.text.iter_mut(), { + fun(t); + }); + let _ = async_for!(t in comp_line.secondary_text.iter_mut(), { + fun(t); + }); + if let Some(t) = comp_line.counter_text.as_mut() { + fun(t); + } + if let Some(v) = comp_line.split_name.as_mut() { + let _ = async_for!(t in v.iter_mut(), { + fun(t); + }); + } +} diff --git a/compiler-types/src/lib.rs b/compiler-types/src/lib.rs index 5a620858..80c3216f 100644 --- a/compiler-types/src/lib.rs +++ b/compiler-types/src/lib.rs @@ -27,6 +27,8 @@ pub struct ExecDoc { pub preface: Vec>, /// The route pub route: Vec, + /// Overall diagnostics (that don't apply to any line) + pub diagnostics: Vec, } /// Metadata of the route project @@ -58,12 +60,16 @@ pub struct RouteMetadata { #[ts(export)] pub struct DocTag { /// Bold style + #[serde(default)] bold: bool, /// Italic style + #[serde(default)] italic: bool, /// Underline style + #[serde(default)] underline: bool, /// Strikethrough style + #[serde(default)] strikethrough: bool, /// Color of the text color: Option, diff --git a/compiler-wasm/src/loader_file.rs b/compiler-wasm/src/loader_file.rs index 11917c7d..b1ceaab0 100644 --- a/compiler-wasm/src/loader_file.rs +++ b/compiler-wasm/src/loader_file.rs @@ -33,7 +33,7 @@ impl FileLoader { #[async_trait::async_trait(?Send)] impl ResourceLoader for FileLoader { async fn load_raw(&self, path: &str) -> PackerResult> { - yield_now!()?; + let _ = yield_now!(); let result: Result = async { let promise = self .load_file diff --git a/compiler-wasm/src/loader_url.rs b/compiler-wasm/src/loader_url.rs index f229594b..2c34dc34 100644 --- a/compiler-wasm/src/loader_url.rs +++ b/compiler-wasm/src/loader_url.rs @@ -30,7 +30,7 @@ impl UrlLoader { #[async_trait::async_trait(?Send)] impl ResourceLoader for UrlLoader { async fn load_raw(&self, url: &str) -> PackerResult> { - yield_now!()?; + let _ = yield_now!(); let result: Result = async { let promise = self .fetch diff --git a/docs/src/.vitepress/config.ts b/docs/src/.vitepress/config.ts index 97cb7c26..ec21f36b 100644 --- a/docs/src/.vitepress/config.ts +++ b/docs/src/.vitepress/config.ts @@ -1,5 +1,6 @@ import { TransformContext, defineConfig } from "vitepress"; import { writingRoutesNav, writingRoutesSidebar } from "./nav"; +import { pluginsNav, pluginsSideBar } from "./nav"; // https://vitepress.dev/reference/site-config export default defineConfig({ @@ -63,24 +64,13 @@ export default defineConfig({ nav: [ { text: "Home", link: "/" }, writingRoutesNav, - { text: "Writing Plugins", link: "/plugin/" }, + pluginsNav, { text: "Developer", link: "/developer/" }, ], sidebar: { ...writingRoutesSidebar, - "/usage/": [ - { - text: "Examples", - items: [ - { - text: "Markdown Examples", - link: "/markdown-examples", - }, - { text: "Runtime API Examples", link: "/api-examples" }, - ], - }, - ], + ...pluginsSideBar, "/developer/": [ { text: "Web Client", diff --git a/docs/src/.vitepress/nav/index.ts b/docs/src/.vitepress/nav/index.ts index 554d8fef..b6fc1a2d 100644 --- a/docs/src/.vitepress/nav/index.ts +++ b/docs/src/.vitepress/nav/index.ts @@ -1 +1,2 @@ export * from "./writing-routes"; +export * from "./plugins"; diff --git a/docs/src/.vitepress/nav/plugins.ts b/docs/src/.vitepress/nav/plugins.ts new file mode 100644 index 00000000..347a8b1f --- /dev/null +++ b/docs/src/.vitepress/nav/plugins.ts @@ -0,0 +1,26 @@ +export const pluginsNav = { + text: "Plugins", + link: "/plugin/", +}; + +export const pluginsSideBar = { + "/plugin/": [ + { + text: "Built-in Plugins", + items: [ + { text: "Getting Started", link: "/plugin/getting-started" }, + { text: "Link", link: "/plugin/link" }, + ], + }, + // { + // text: "JavaScript Plugins", + // items: [ + // // { text: "Icons", link: "/route/config/icons" }, + // // { text: "Tags", link: "/route/config/tags" }, + // // { text: "Presets", link: "/route/config/presets" }, + // // { text: "Map", link: "/route/config/map" }, + // // { text: "Other", link: "/route/config/other" }, + // ], + // }, + ], +}; diff --git a/docs/src/.vitepress/nav/writing-plugins.ts b/docs/src/.vitepress/nav/writing-plugins.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/src/.vitepress/nav/writing-routes.ts b/docs/src/.vitepress/nav/writing-routes.ts index bdfee0f7..859967a0 100644 --- a/docs/src/.vitepress/nav/writing-routes.ts +++ b/docs/src/.vitepress/nav/writing-routes.ts @@ -31,6 +31,7 @@ export const writingRoutesSidebar = { { text: "Tags", link: "/route/config/tags" }, { text: "Presets", link: "/route/config/presets" }, { text: "Map", link: "/route/config/map" }, + { text: "Plugins", link: "/plugin/getting-started" }, { text: "Other", link: "/route/config/other" }, ], }, diff --git a/docs/src/api-examples.md b/docs/src/api-examples.md deleted file mode 100644 index 6bd8bb5c..00000000 --- a/docs/src/api-examples.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -outline: deep ---- - -# Runtime API Examples - -This page demonstrates usage of some of the runtime APIs provided by VitePress. - -The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files: - -```md - - -## Results - -### Theme Data -
{{ theme }}
- -### Page Data -
{{ page }}
- -### Page Frontmatter -
{{ frontmatter }}
-``` - - - -## Results - -### Theme Data -
{{ theme }}
- -### Page Data -
{{ page }}
- -### Page Frontmatter -
{{ frontmatter }}
- -## More - -Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata). diff --git a/docs/src/markdown-examples.md b/docs/src/markdown-examples.md deleted file mode 100644 index 8e55eb8a..00000000 --- a/docs/src/markdown-examples.md +++ /dev/null @@ -1,85 +0,0 @@ -# Markdown Extension Examples - -This page demonstrates some of the built-in markdown extensions provided by VitePress. - -## Syntax Highlighting - -VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting: - -**Input** - -```` -```js{4} -export default { - data () { - return { - msg: 'Highlighted!' - } - } -} -``` -```` - -**Output** - -```js{4} -export default { - data () { - return { - msg: 'Highlighted!' - } - } -} -``` - -## Custom Containers - -**Input** - -```md -::: info -This is an info box. -::: - -::: tip -This is a tip. -::: - -::: warning -This is a warning. -::: - -::: danger -This is a dangerous warning. -::: - -::: details -This is a details block. -::: -``` - -**Output** - -::: info -This is an info box. -::: - -::: tip -This is a tip. -::: - -::: warning -This is a warning. -::: - -::: danger -This is a dangerous warning. -::: - -::: details -This is a details block. -::: - -## More - -Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown). diff --git a/docs/src/plugin/getting-started.md b/docs/src/plugin/getting-started.md new file mode 100644 index 00000000..7dac5d2a --- /dev/null +++ b/docs/src/plugin/getting-started.md @@ -0,0 +1,34 @@ +# Getting Started +:::tip +The plugin system is currently unstable. +::: +The principle of the plugin system is to separate core Celer functionalities from additional (and mostly optional) functionalities. + +A plugin in Celer is a piece of program that runs as part of the compiler. The process goes as the following: + +1. The compiler parses the income files and celer-specific syntax like presets +2. The compiler hands the compiled document to a plugin +3. The plugin is free to make any changes to the document. Then it hands the document to the next plugin in line. +4. After the last plugin is done modifying the document, it hands the document back to the compiler. + + +## Configuration +To add a plugin to the compiler, use the `plugins` property in your `config`. + +The example below adds the built-in [Link Plugin](./link.md) to the compiler, which transforms `link` tags into clickable links. +```yaml +config: +- plugins: + - use: link +``` +:::tip +Note that the `use` property takes `"link"`, which is not any of the syntax mentioned in +[File structure](../route/file-structure.md). This signals Celer that you want to use a built-in +plugin. Built-in plugins are implemented in Rust and has higher performance. +::: + +## Built-in Plugins +Here is a list of all built-in plugins. The `ID` column is what you put after `use` in the config. +|Name|ID|Description| +|-|-|-| +|[Link](./link.md)|`link`|Turns `link` tags into clickable links| diff --git a/docs/src/plugin/index.md b/docs/src/plugin/index.md new file mode 100644 index 00000000..a19ffb9a --- /dev/null +++ b/docs/src/plugin/index.md @@ -0,0 +1,12 @@ +# Plugins +:::tip +The plugin system is currently unstable. +::: +This section contains guides for how to utilize Celer's plugin system to add +extra features to your route doc. + +Start with [Getting Started](./getting-started.md) to get a basic understanding +of what plugins can do in Celer. Then you can take a look at the built-in plugins +like the [Link Plugin](./link.md). + + diff --git a/docs/src/plugin/link.md b/docs/src/plugin/link.md new file mode 100644 index 00000000..cf87118b --- /dev/null +++ b/docs/src/plugin/link.md @@ -0,0 +1,61 @@ +# Link Plugin +:::tip +The plugin system is currently unstable. +::: +The link plugin transforms `link` tags in texts into clickable link. + +Add the plugin with +```yaml +config: +- plugins: + use: link +``` + +## Examples +Add clickable links in primary, secondary, or note section of a line +```yaml +config: +- plugins: + use: link +route: +- Example Section: + - Link in the notes: + notes: .link(https://example.com) + - Link in the comment: + comment: .link(https://example.com) + - .link(https://example.com) +``` +Override the link text by putting it in `[` and `]` +```yaml +config: +- plugins: + use: link +route: +- Example Section: + - Example Line: + # Text will show "click here to see the notes", with "here" clickable + notes: Click .link([here]https://example.com) to see the notes +``` +Override the link style by defining the `link` tag in config + +```yaml +config: +- plugins: + use: link + tags: + link: + italic: true # show links in italic +route: +- Example Section: + - Example Line: + # "here" will be clickable and will be italic + notes: Click .link([here]https://example.com) to see the notes +``` +:::tip +The link already inherits styles from the Celer UI (with a different color and underline). +Only override the style if needed! +::: +:::warning +Specifying `underline: false` will probably have no effect because Celer UI renders +links with underlines. +::: diff --git a/docs/src/route/built-in-plugins.md b/docs/src/route/built-in-plugins.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/src/route/configuration.md b/docs/src/route/configuration.md index 7b5ab419..22e5f2bd 100644 --- a/docs/src/route/configuration.md +++ b/docs/src/route/configuration.md @@ -12,6 +12,7 @@ In case you want to make your own, here are the available properties: |`icons`|Add icon definition. See [Icons](./config/icons.md) for detail| |`tags`|Add tag definition for use in Rich Text. See [Tags](./config/tags.md) for detail| |`presets`|Add preset definition. See [Presets](./config/presets.md) for detail| +|`plugins`|Add plugin definition. See [Plugins](../plugin/getting-started.md) for detail| |`map`|Define map properties. See [Map](./config/map.md) for detail| Configurations are meant to be composed and reused with other configurations. @@ -22,14 +23,20 @@ The compiler will give an error if multiple configurations define the map. ```yaml # project.yaml config: -- use: Pistonite/celer/presets/botw-map.yaml -- use: Pistonite/celer/presets/botw-presets.yaml +- use: Pistonite/celer/presets/botw/map.yaml +- use: someone/someones-preset/awesome.yaml +- use: ./path/to/local.yaml - icons: example-icon: use: hello/world/example.png tags: colorful: color: blue + +# path/to/local.yaml +presets: + HelloWorld: + text: Hello, world! ``` ## Configuration Files @@ -44,6 +51,10 @@ tags: colorful: color: blue ``` +:::warning +Note that properties in `something.yaml` (`icons` and `tags`) don't have a +`-` in front because the file needs to contain a mapping, not an array at the root level. +::: Be careful that top-level `use` is not permitted. The following config file is invalid for others to include with `use`: @@ -51,7 +62,37 @@ is invalid for others to include with `use`: # something.yaml use: another/file/something.yaml ``` +:::warning +CAUTION: The example above is for what is NOT supported +::: :::tip You can still use `use` in the config properties themselves, like the example above in the `icons` property ::: + +## Grouping +Celer does NOT support complex grouping structures for dependency. For example, +you CANNOT make a config file A which includes 3 other config files, and include +the 3 files by including config file A. +```yaml +# This maybe desired in some cases, but Celer does not support this + +# project.yaml +config: +- use: ./A.yaml + +# A.yaml +- use: ./1.yaml +- use: ./2.yaml +- use: ./3.yaml +``` +:::warning +CAUTION: The example above is for what is NOT supported +::: + +The reason for not supporting this is that Celer discourages complex dependency structure. +Complex dependencies make it tricker to debug when a configuration is not functioning and +make it hard to display meaningful error messages. + +There are workarounds to this if you really want this behavior, particularly when distributing the config files. +For example, you can generate `A.yaml` from the 3 other files ahead of time. Or you could use CI like GitHub Actions to do that automatically. diff --git a/docs/src/route/customizing-lines.md b/docs/src/route/customizing-lines.md index 963741dd..b99a84ba 100644 --- a/docs/src/route/customizing-lines.md +++ b/docs/src/route/customizing-lines.md @@ -155,9 +155,8 @@ The `presets` property allow you to define additional presets for the line. See [Using Presets](./using-presets.md) for details. ## Other Properties -Built-in and third-party plugins may let you set additional properties. Check the -documentation for [Built-in plugins](./built-in-plugins) or the third-party plugin -you are using. +If you are using [Plugins](../plugin/index.md). You may be able to specify additional properties. Please refer to the +documentation for the plugin you are using. If you specify a property that is not recognized by Celer or any plugin, you will see a warning message saying that property is unused. diff --git a/docs/src/route/getting-started.md b/docs/src/route/getting-started.md index dbd8c219..1d0fe23f 100644 --- a/docs/src/route/getting-started.md +++ b/docs/src/route/getting-started.md @@ -17,7 +17,7 @@ config: [] The file names are case-sensitive. This file should be named `project.yaml`, not `Project.yaml` or `project.yml` ::: -Then, create a `main.yaml` file with the following content: +Then, create a `main.yaml` file in the same folder with the following content: ```yaml - hello world! - Section 1: diff --git a/docs/src/user/index.md b/docs/src/user/index.md deleted file mode 100644 index 99f10893..00000000 --- a/docs/src/user/index.md +++ /dev/null @@ -1 +0,0 @@ -# todo \ No newline at end of file