From 1a2ea902b9a2df7c6e2b95acaf8e7e9e2c43851c Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Wed, 10 Aug 2022 07:58:06 +0300 Subject: feat: implement shortcut directive Sponsored-by: author --- riki.md | 114 +++++++++++++++++++++++++------------------ src/directive/brokenlinks.rs | 4 +- src/directive/img.rs | 4 +- src/directive/inline.rs | 4 +- src/directive/meta.rs | 4 +- src/directive/mod.rs | 34 ++++++++----- src/directive/pagestats.rs | 4 +- src/directive/shortcut.rs | 29 ++++++----- src/directive/tag.rs | 4 +- src/directive/toc.rs | 4 +- src/parser.rs | 48 ++++++++++++------ src/site.rs | 39 +++++++++++++++ src/token.rs | 8 +++ src/wikitext.rs | 14 +++++- 14 files changed, 212 insertions(+), 102 deletions(-) diff --git a/riki.md b/riki.md index e5ae0f0..912dbac 100644 --- a/riki.md +++ b/riki.md @@ -334,7 +334,7 @@ then AST of site/page.mdwn matches that of output/page.html * [x] done ~~~ -## Definition list +### Definition list _Requirement: Markup indicating use of a definition list should be flagged as an error._ @@ -380,40 +380,7 @@ foo ~~~ -## Input files other than Markdown - -_Requirement: Input files that aren't Markdown files must be copied -into the destination directory as-is._ - -~~~scenario -given an installed riki -given file site/image.jpg from image -when I run riki build --plain-body site output -then files site/image.jpg and output/image.jpg match -~~~ - - -~~~{#image .file} -# Dummy -Pretend this is an image. -~~~ - -## Input files in sub=directories - -_Requirement: If an source page or file is in a sub-directory, it -should be put in the corresponding sub-directory in the target -directory._ - -~~~scenario -given an installed riki -given file site/foo/page.mdwn from image -given file site/bar/image.jpg from para -when I run riki build --plain-body site output -then AST of site/foo/page.mdwn matches that of output/foo/page.html -then files site/bar//image.jpg and output/bar/image.jpg match -~~~ - -## Wiki links to other pages on the site +### Wiki links to other pages on the site _Requirement: Pages can link to other pages on the site, the same way ikiwiki does, including subpages._ @@ -442,7 +409,7 @@ Note the uppercase link to the `child` page in the test page below. [[CHILD]] ~~~ -## Wiki links to pages that don't exist +### Wiki links to pages that don't exist _Requirement: Linking to a page that doesn't exist is an error._ @@ -459,6 +426,27 @@ then command fails ## Directives +### `img` + +_Requirement: the `img` directive embeds an image in the generated +HTML page._ + +~~~scenario +given an installed riki +given file site/index.mdwn from img +given file site/img.jpg from jpeg +when I run riki build site output +then file output/index.html contains "Yo" [[!meta title=Yo]]] ~~~ -### `img` +### `shortcut` -_Requirement: the `img` directive embeds an image in the generated -HTML page._ +_Requirement: the `shortcut` directive created a shortcut that looks +like a directive._ ~~~scenario given an installed riki -given file site/index.mdwn from img -given file site/img.jpg from jpeg +given file site/index.mdwn from shortcut when I run riki build site output -then file output/index.html contains "foo!123" ~~~ -~~~{#img .file .markdown} -[[!img img.jpg]]] -~~~ +~~~{#shortcut .file .markdown} +[[!shortcut name="foo" url="https://example.com/foo/%s" desc="foo!%s"]] -~~~{#jpeg .file} -This is a dummy JPEG image. +[[!foo 123]] ~~~ ## Source file tree @@ -530,6 +515,41 @@ then stdout doesn't contain "index.mdwn~" then stdout doesn't contain "#index.mdwn#" ~~~ + + +## Input files other than Markdown + +_Requirement: Input files that aren't Markdown files must be copied +into the destination directory as-is._ + +~~~scenario +given an installed riki +given file site/image.jpg from image +when I run riki build --plain-body site output +then files site/image.jpg and output/image.jpg match +~~~ + + +~~~{#image .file} +# Dummy +Pretend this is an image. +~~~ + +## Input files in sub-directories + +_Requirement: If an source page or file is in a sub-directory, it +should be put in the corresponding sub-directory in the target +directory._ + +~~~scenario +given an installed riki +given file site/foo/page.mdwn from image +given file site/bar/image.jpg from para +when I run riki build --plain-body site output +then AST of site/foo/page.mdwn matches that of output/foo/page.html +then files site/bar//image.jpg and output/bar/image.jpg match +~~~ + ## Output directory tree ### No markdown files in output tree diff --git a/src/directive/brokenlinks.rs b/src/directive/brokenlinks.rs index c6364a4..3091be9 100644 --- a/src/directive/brokenlinks.rs +++ b/src/directive/brokenlinks.rs @@ -20,8 +20,8 @@ impl BrokenLinks { } } -impl From for BrokenLinks { - fn from(_p: ParsedDirective) -> Self { +impl From<&ParsedDirective> for BrokenLinks { + fn from(_p: &ParsedDirective) -> Self { Self::new() } } diff --git a/src/directive/img.rs b/src/directive/img.rs index dea2625..307f035 100644 --- a/src/directive/img.rs +++ b/src/directive/img.rs @@ -27,8 +27,8 @@ impl Img { } } -impl From for Img { - fn from(p: ParsedDirective) -> Self { +impl From<&ParsedDirective> for Img { + fn from(p: &ParsedDirective) -> Self { let unnamed = p.unnamed_args().pop().unwrap(); Img::new(unnamed.into()) } diff --git a/src/directive/inline.rs b/src/directive/inline.rs index efaf7ee..5d4f786 100644 --- a/src/directive/inline.rs +++ b/src/directive/inline.rs @@ -31,8 +31,8 @@ impl Inline { } } -impl From for Inline { - fn from(_p: ParsedDirective) -> Self { +impl From<&ParsedDirective> for Inline { + fn from(_p: &ParsedDirective) -> Self { Inline::new() } } diff --git a/src/directive/meta.rs b/src/directive/meta.rs index da595eb..6f47d45 100644 --- a/src/directive/meta.rs +++ b/src/directive/meta.rs @@ -25,8 +25,8 @@ impl Meta { } } -impl From for Meta { - fn from(p: ParsedDirective) -> Self { +impl From<&ParsedDirective> for Meta { + fn from(p: &ParsedDirective) -> Self { let mut meta = Meta::default(); let args = p.args(); if let Some(title) = args.get("title") { diff --git a/src/directive/mod.rs b/src/directive/mod.rs index ee96bfc..4697663 100644 --- a/src/directive/mod.rs +++ b/src/directive/mod.rs @@ -26,39 +26,47 @@ impl TryFrom for Directive { type Error = SiteError; fn try_from(p: ParsedDirective) -> Result { + Self::try_from(&p) + } +} + +impl TryFrom<&ParsedDirective> for Directive { + type Error = SiteError; + + fn try_from(p: &ParsedDirective) -> Result { let d = match p.name() { "simple" => { - Self::check_args(&p, &[], &[], false)?; + Self::check_args(p, &[], &[], false)?; Directive::Simple } "unnamedarg" => { - Self::check_args(&p, &[], &[], true)?; + Self::check_args(p, &[], &[], true)?; Directive::UnnamedArg } "simplearg" => { - Self::check_args(&p, &["foo"], &[], false)?; + Self::check_args(p, &["foo"], &[], false)?; Directive::SimpleArg } "quotedarg" => { - Self::check_args(&p, &["bar"], &[], false)?; + Self::check_args(p, &["bar"], &[], false)?; Directive::QuotedArg } "multilinearg" => { - Self::check_args(&p, &["yo"], &["then", "else"], false)?; + Self::check_args(p, &["yo"], &["then", "else"], false)?; Directive::MultilineArg } "brokenlinks" => { - Self::check_args(&p, BrokenLinks::REQUIRED, BrokenLinks::ALLOWED, BrokenLinks::ALLOW_ANY_UNNAMED)?; + Self::check_args(p, BrokenLinks::REQUIRED, BrokenLinks::ALLOWED, BrokenLinks::ALLOW_ANY_UNNAMED)?; Directive::BrokenLinks(BrokenLinks::from(p)) } "img" => { - Self::check_args(&p, Img::REQUIRED, Img::ALLOWED, Img::ALLOW_ANY_UNNAMED)?; + Self::check_args(p, Img::REQUIRED, Img::ALLOWED, Img::ALLOW_ANY_UNNAMED)?; Directive::Img(Img::from(p)) } "inline" => { Self::check_args( - &p, + p, Inline::REQUIRED, Inline::ALLOWED, Inline::ALLOW_ANY_UNNAMED, @@ -66,12 +74,12 @@ impl TryFrom for Directive { Directive::Inline(Inline::from(p)) } "meta" => { - Self::check_args(&p, Meta::REQUIRED, Meta::ALLOWED, Meta::ALLOW_ANY_UNNAMED)?; + Self::check_args(p, Meta::REQUIRED, Meta::ALLOWED, Meta::ALLOW_ANY_UNNAMED)?; Directive::Meta(Meta::from(p)) } "pagestats" => { Self::check_args( - &p, + p, PageStats::REQUIRED, PageStats::ALLOWED, PageStats::ALLOW_ANY_UNNAMED, @@ -79,15 +87,15 @@ impl TryFrom for Directive { Directive::PageStats(PageStats::from(p)) } "shortcut" => { - Self::check_args(&p, Shortcut::REQUIRED, Shortcut::ALLOWED, Shortcut::ALLOW_ANY_UNNAMED)?; + Self::check_args(p, Shortcut::REQUIRED, Shortcut::ALLOWED, Shortcut::ALLOW_ANY_UNNAMED)?; Directive::Shortcut(Shortcut::from(p)) } "tag" => { - Self::check_args(&p, Tag::REQUIRED, Tag::ALLOWED, Tag::ALLOW_ANY_UNNAMED)?; + Self::check_args(p, Tag::REQUIRED, Tag::ALLOWED, Tag::ALLOW_ANY_UNNAMED)?; Directive::Tag(Tag::from(p)) } "toc" => { - Self::check_args(&p, Toc::REQUIRED, Toc::ALLOWED, Toc::ALLOW_ANY_UNNAMED)?; + Self::check_args(p, Toc::REQUIRED, Toc::ALLOWED, Toc::ALLOW_ANY_UNNAMED)?; Directive::Toc(Toc::from(p)) } _ => return Err(SiteError::UnknownDirective(p.name().into())), diff --git a/src/directive/pagestats.rs b/src/directive/pagestats.rs index 1a6f811..164ac9c 100644 --- a/src/directive/pagestats.rs +++ b/src/directive/pagestats.rs @@ -20,8 +20,8 @@ impl PageStats { } } -impl From for PageStats { - fn from(_p: ParsedDirective) -> Self { +impl From<&ParsedDirective> for PageStats { + fn from(_p: &ParsedDirective) -> Self { Self::new() } } diff --git a/src/directive/shortcut.rs b/src/directive/shortcut.rs index 03fd648..534300a 100644 --- a/src/directive/shortcut.rs +++ b/src/directive/shortcut.rs @@ -1,27 +1,34 @@ use crate::error::SiteError; use crate::page::PageMeta; -use crate::site::Site; +use crate::site::{Shortcut as S, Site}; use crate::wikitext::ParsedDirective; -#[derive(Debug, Default, Eq, PartialEq)] -pub struct Shortcut {} +#[derive(Debug, Eq, PartialEq)] +pub struct Shortcut { + shortcut: S, +} impl Shortcut { pub const REQUIRED: &'static [&'static str] = &["desc", "name", "url"]; pub const ALLOWED: &'static [&'static str] = &[]; - pub const ALLOW_ANY_UNNAMED: bool = true; + pub const ALLOW_ANY_UNNAMED: bool = false; - pub fn new() -> Self { - Self::default() + pub fn new(shortcut: S) -> Self { + Self { shortcut } } - pub fn process(&self, _site: &Site, _meta: &mut PageMeta) -> Result { - Ok("FIXME:inline".into()) + pub fn process(&self, site: &mut Site, _meta: &mut PageMeta) -> Result { + site.add_shortcut(self.shortcut.clone()); + Ok("".into()) } } -impl From for Shortcut { - fn from(_p: ParsedDirective) -> Self { - Self::new() +impl From<&ParsedDirective> for Shortcut { + fn from(p: &ParsedDirective) -> Self { + let args = p.args(); + let name = args.get("name").unwrap(); + let desc = args.get("desc").unwrap(); + let url = args.get("url").unwrap(); + Self::new(S::new(name, desc, url)) } } diff --git a/src/directive/tag.rs b/src/directive/tag.rs index baf4561..c64acfc 100644 --- a/src/directive/tag.rs +++ b/src/directive/tag.rs @@ -22,8 +22,8 @@ impl Tag { } } -impl From for Tag { - fn from(p: ParsedDirective) -> Self { +impl From<&ParsedDirective> for Tag { + fn from(p: &ParsedDirective) -> Self { let tags = p.unnamed_args().iter().map(|s| s.to_string()).collect(); Tag::new(tags) } diff --git a/src/directive/toc.rs b/src/directive/toc.rs index 6db46d2..cc94be4 100644 --- a/src/directive/toc.rs +++ b/src/directive/toc.rs @@ -20,8 +20,8 @@ impl Toc { } } -impl From for Toc { - fn from(_p: ParsedDirective) -> Self { +impl From<&ParsedDirective> for Toc { + fn from(_p: &ParsedDirective) -> Self { Self::new() } } diff --git a/src/parser.rs b/src/parser.rs index 662ffd0..d283d90 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,9 +1,7 @@ -use crate::directive::Directive; use crate::error::SiteError; use crate::token::{Token, TokenParser, TokenPatterns}; use crate::wikitext::{ParsedDirective, Snippet, WikiLink}; use std::collections::HashMap; -use std::convert::TryFrom; #[derive(Debug)] pub struct WikitextParser { @@ -102,8 +100,7 @@ impl WikitextParser { _ => panic!("yikes: {:?}", self.tokens), } } - let p = ParsedDirective::new(&name, args)?; - Snippet::Directive(Directive::try_from(p)?) + Snippet::Directive(ParsedDirective::new(&name, args)?) } [Token::Bang, Token::OpenBracket, ..] => { let mut link_text = String::new(); @@ -200,8 +197,16 @@ impl WikitextParser { #[cfg(test)] mod test { - use super::{Directive, Snippet, TokenPatterns, WikiLink, WikitextParser}; - use crate::directive::Img; + use super::{ParsedDirective, Snippet, TokenPatterns, WikiLink, WikitextParser}; + use std::collections::HashMap; + + fn parsed_directive(name: &str, kv: &[(&str, &str)]) -> ParsedDirective { + ParsedDirective::new( + name, + HashMap::from_iter(kv.iter().map(|(k, v)| (k.to_string(), v.to_string()))), + ) + .unwrap() + } #[test] fn empty_input() { @@ -285,7 +290,7 @@ mod test { let mut p = WikitextParser::new("[[!simple]]", &patterns); assert_eq!( p.parse().unwrap(), - Some(Snippet::Directive(Directive::Simple)) + Some(Snippet::Directive(parsed_directive("simple", &[]))) ); assert_eq!(p.parse().unwrap(), None); } @@ -296,7 +301,10 @@ mod test { let mut p = WikitextParser::new("[[!unnamedarg foo.jpg]]", &patterns); assert_eq!( p.parse().unwrap(), - Some(Snippet::Directive(Directive::UnnamedArg)) + Some(Snippet::Directive(parsed_directive( + "unnamedarg", + &[("foo.jpg", "")] + ))) ); assert_eq!(p.parse().unwrap(), None); } @@ -307,7 +315,10 @@ mod test { let mut p = WikitextParser::new("[[!simplearg foo=bar]]", &patterns); assert_eq!( p.parse().unwrap(), - Some(Snippet::Directive(Directive::SimpleArg)) + Some(Snippet::Directive(parsed_directive( + "simplearg", + &[("foo", "bar")] + ))) ); assert_eq!(p.parse().unwrap(), None); } @@ -318,7 +329,10 @@ mod test { let mut p = WikitextParser::new(r#"[[!quotedarg bar="foobar"]]"#, &patterns); assert_eq!( p.parse().unwrap(), - Some(Snippet::Directive(Directive::QuotedArg)) + Some(Snippet::Directive(parsed_directive( + "quotedarg", + &[("bar", "foobar")] + ))) ); assert_eq!(p.parse().unwrap(), None); } @@ -328,12 +342,15 @@ mod test { let patterns = TokenPatterns::default(); let mut p = WikitextParser::new( r#"[[!multilinearg yo="""foo - bar"""]]"#, +bar"""]]"#, &patterns, ); assert_eq!( p.parse().unwrap(), - Some(Snippet::Directive(Directive::MultilineArg)) + Some(Snippet::Directive(parsed_directive( + "multilinearg", + &[("yo", "foo\nbar")] + ))) ); assert_eq!(p.parse().unwrap(), None); } @@ -344,9 +361,10 @@ mod test { let mut p = WikitextParser::new(r#"[[!img foo.jpg class=image]]"#, &patterns); assert_eq!( p.parse().unwrap(), - Some(Snippet::Directive(Directive::Img(Img::new( - "foo.jpg".into() - )))) + Some(Snippet::Directive(parsed_directive( + "img", + &[("foo.jpg", ""), ("class", "image")] + ))) ); assert_eq!(p.parse().unwrap(), None); } diff --git a/src/site.rs b/src/site.rs index 3200b58..f1ff333 100644 --- a/src/site.rs +++ b/src/site.rs @@ -21,6 +21,7 @@ pub struct Site { name_queue: Vec, page_queue: PageSet, whatchanged: HashMap, + shortcuts: HashMap, } impl Site { @@ -41,6 +42,7 @@ impl Site { name_queue: vec![], page_queue: PageSet::default(), whatchanged: HashMap::new(), + shortcuts: HashMap::new(), } } @@ -267,6 +269,14 @@ impl Site { } false } + + pub fn add_shortcut(&mut self, shortcut: Shortcut) { + self.shortcuts.insert(shortcut.name().into(), shortcut); + } + + pub fn shortcut(&self, name: &str) -> Option<&Shortcut> { + self.shortcuts.get(name) + } } #[derive(Default)] @@ -290,6 +300,35 @@ impl PageSet { } } +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Shortcut { + name: String, + desc: String, + url: String, +} + +impl Shortcut { + pub fn new(name: &str, desc: &str, url: &str) -> Self { + Self { + name: name.into(), + desc: desc.into(), + url: url.into(), + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn desc(&self, arg: &str) -> String { + self.desc.replace("%s", arg) + } + + pub fn url(&self, arg: &str) -> String { + self.url.replace("%s", arg) + } +} + #[cfg(test)] mod test { use super::{NameBuilder, Site, SiteError, WikitextPage}; diff --git a/src/token.rs b/src/token.rs index 5109ce1..eedf655 100644 --- a/src/token.rs +++ b/src/token.rs @@ -223,6 +223,14 @@ mod test { assert_eq!(p.parse(), Token::End); } + #[test] + fn number_word() { + let patterns = TokenPatterns::default(); + let mut p = parser("123", &patterns); + assert_eq!(p.parse(), Token::Word("123".into())); + assert_eq!(p.parse(), Token::End); + } + #[test] fn complex_word() { let patterns = TokenPatterns::default(); diff --git a/src/wikitext.rs b/src/wikitext.rs index b425021..91ac30b 100644 --- a/src/wikitext.rs +++ b/src/wikitext.rs @@ -9,7 +9,7 @@ use std::path::Path; pub enum Snippet { Markdown(String), WikiLink(WikiLink), - Directive(Directive), + Directive(ParsedDirective), } impl Snippet { @@ -22,7 +22,17 @@ impl Snippet { trace!("resolved {} to {}", w.target(), resolved.display()); format!("[{}]({})", w.link_text(), resolved.display()) } - Snippet::Directive(d) => d.process(site, meta)?, + Snippet::Directive(p) => { + let e = Directive::try_from(p); + if let Ok(d) = e { + d.process(site, meta)? + } else if let Some(shortcut) = site.shortcut(p.name()) { + let arg = p.unnamed_args().get(0).unwrap().to_string(); + format!("[{}]({})", shortcut.desc(&arg), shortcut.url(&arg)) + } else { + return Err(e.unwrap_err()); + } + } }; Ok(s) } -- cgit v1.2.1