diff --git a/home/modules/contribute/pages/term.adoc b/home/modules/contribute/pages/term.adoc new file mode 100644 index 0000000000..58584c6abc --- /dev/null +++ b/home/modules/contribute/pages/term.adoc @@ -0,0 +1,98 @@ += TERM macro! + +Inspired by the text adventure game language Inform7, and in particular its features for formatting the names of things. +http://inform7.com/book/WI_5_3.html + +Inform7 is based around objects, and finds clever ways within its (English-like) syntax to describe and name them, +and also express relationships between them. + +It seems like something a documentation system might want to do, only instead of "The brass lantern is in the chest" +we might want to declare that "An Application Service is a kind of Service. It has zero or more Application Endpoints." + +Anyway, this macro just looks at the formatting of names for now... + +== Example + +With the following config: + +[source,yml] +---- +asciidoc: + attributes: + "term-App Service": Application Service + "term-N1QL": "SQL++" + "term-N1QL-indefinite": an +---- + +We get the following features: + +== Passthrough + +[source,asciidoc] +---- +term:[The Frobnitzer] is awesome! +---- +> term:[The Frobnitzer] is awesome! +// "The Frobnitzer is awesome!" + +Yes, that's right! If we haven't defined any terms, then this is a glorified pass-through macro! +Still, you could imagine we could potentially format terms differently or something, and it will make it easier to grep for. + +== Swap names + +[source,asciidoc] +---- +Deploy term:[an App Service] +---- +> Deploy term:[an App Service] +// "Deploy an Application Service" + +This is a little more interesting: because we've renamed App Service in the meantime, we get to magically update the name. + +== Indefinite articles + +[source,asciidoc] +---- +Let's run term:[a N1QL] query! +---- +> Let's run term:[a N1QL] query! +// "Let's run an SQL++ query!" + +This corrects the indefinite article. +(Assuming we pronounce it Ess Queue Ell of course...) +There is a default handler that will pick the appropriate article based on vowels, +but where we need fine control, as in this ambiguous case, we can specify it in config. + +== Handle capitalization + +[source,asciidoc] +---- +term:[A N1QL] query a day keeps the DBA away. +---- +> term:[A N1QL] query a day keeps the DBA away. +// "An SQL++ query a day keeps the DBA away." + +If we wanted to use Asciidoc attributes instead, we could simply define `{term-n1ql}` and `{term-a-n1ql}` and substitute those. +But then we don't handle capitalization... + +Annoyingly Asciidoc doesn't distinguish `{term-A-n1ql}` case-sensitive, so we'd have to do `{term-a-n1ql-caps}` or similar, which is rather ugly. +Just writing the article on the other hand is intuitive and involves less faffing with include:: of tokens files. + +== More ideas + +This idea might be generally useful as it would allow us to distinguish terms for various purposes. +Other uses might include: + + * formatting terms differently + * linking the first usage of a term on a page to a tag or definition + * indexing where terms are used + +Future enhancements could include + + * Parsing plurals + +Simplifications could include: + + * remove the attribute parsing entirely, and simply substitute whole strings. + (e.g. we would provide literal terms for "Foo" and "A foo" and "Foos") + This would reduce complexity and potential fragility, and make the behaviour more predictable, if slightly more tedious. diff --git a/lib/inline-term-macro.js b/lib/inline-term-macro.js new file mode 100644 index 0000000000..fb86c1a490 --- /dev/null +++ b/lib/inline-term-macro.js @@ -0,0 +1,81 @@ +/* + * + * @author Hakim Cassimally + */ + + + +const capitalize = (string) => string.charAt(0).toUpperCase() + string.slice(1) +const compose = (f1, f2) => (arg) => f1(f2(arg)) + +const _indefinite = (term) => term.indefinite || term.term.match(/^[aeiou]/i) ? 'an' : 'a' + +// prefix handlers +const normal = (prefix) => (term) => prefix ? `${prefix} ${term.term}` : term.term +const indefinite = (term) => `${_indefinite(term)} ${term.term}` + +const prefix_handlers = { + a: indefinite, + an: indefinite, + A: compose(capitalize, indefinite), + An: compose(capitalize, indefinite), +} + +function initInlineTermMacro ({ mapping }) { + return function () { + + this.parseContentAs('text') + this.matchFormat('short') + + this.process((parent, _, attrs) => { + + const text = attrs.text + + const knownTerms = Object.keys(mapping).join('|') + + const match = + text.match(new RegExp(`^(?${knownTerms})$`)) || + text.match(/^(?[Aa]n?|[Tt]he) (?.*)$/) || + text.match(/^(?.*)$/) + + const {term, prefix} = match.groups + + const item = mapping[term] || { term: term } + + const handler = prefix_handlers[prefix] || normal(prefix) + + const output = handler(item) + + return this.createInlinePass(parent, output) + }) + } +} + +function register (registry, context) { + const { config: { attributes } } = context + + const mapping = Object.entries(attributes).reduce((accum, [name, value]) => { + /* + * parse out entries like: + * term-App Service + * term-App Service-indefinite + * term-App Service-plural + * + */ + console.log(name) + const match = name.match(/^term(-(?.*?))(-(?indefinite|plural))?$/) + if (match) { + const term = match.groups.term + const type = match.groups.type || 'term' + accum[term] ||= {} + accum[term][type] = value + } + return accum + }, {}) + + console.log(mapping) + const contextWithMapping = Object.assign({ mapping }, context) + registry.inlineMacro('term', initInlineTermMacro(contextWithMapping)) +} + +module.exports.register = register