diff --git a/docs/myst.yml b/docs/myst.yml index f4eb9f5e1..84bbb3c28 100644 --- a/docs/myst.yml +++ b/docs/myst.yml @@ -49,6 +49,7 @@ project: - directives.mjs - unsplash.mjs - latex.mjs + - templates.mjs error_rules: - rule: link-resolves severity: ignore diff --git a/docs/plugins.md b/docs/plugins.md index 438fd9f3a..8cbc0214c 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -78,3 +78,37 @@ You can now use the directive, for example: If you change the source code you will have to stop and re-start the server to see the results. The types are defined in `myst-common` ([npm](https://www.npmjs.com/package/myst-common), [github](https://github.com/executablebooks/mystmd/tree/main/packages/myst-common)) with the [`DirectiveSpec`](https://github.com/executablebooks/mystmd/blob/9965925030c3fab6f34c20d11eeee7ffdafa73df/packages/myst-common/src/types.ts#L68-L77) and [`RoleSpec`](https://github.com/executablebooks/mystmd/blob/9965925030c3fab6f34c20d11eeee7ffdafa73df/packages/myst-common/src/types.ts#L79-L85) being the main types to implement. + +## Examples of plugins + +The documentation you're reading now defines several of its own plugins to extend MyST functionality. +These are all registered in the documentation's [myst.yml configuration](myst.yml) with syntax like below: + + +```{literalinclude} myst.yml +:start-at: plugins +:end-before: error_rules +``` + +Each plugin is defined as a `.mjs` file in the same folder as the documentation's MyST content. +Below is the contents of each file for reference. + +::::{dropdown} Plugin: Latex rendering +```{literalinclude} latex.mjs +``` +:::: + +::::{dropdown} Plugin: Display an image +```{literalinclude} unsplash.mjs +``` +:::: + +::::{dropdown} Plugin: Custom directive for documenting roles and directives +```{literalinclude} directives.mjs +``` +:::: + +::::{dropdown} Plugin: Render web template options as a table +```{literalinclude} templates.mjs +``` +:::: diff --git a/docs/templates.mjs b/docs/templates.mjs new file mode 100644 index 000000000..5a4f4f0ca --- /dev/null +++ b/docs/templates.mjs @@ -0,0 +1,256 @@ +/** + * Example of a MyST plugin that retrieves MyST template option information + * and displays it as a definition list + */ +import { u } from 'unist-builder'; +import { mystParse } from 'myst-parser'; + +/** + * @typedef MySTTemplateRef + * @type {object} + * @property {string} template - a partial or fully resolved template name + * @property {string} kind - the kind of template, e.g. 'site' + * @property {boolean} fullTitle - show the full template title, or just the name + * @property {number} headingDepth - depth of the generated heading (e.g. 1 for h1) + */ + +/** + * Create a documentation section for a template + * + * This directive simply passes-through the options into an AST node, + * because we can't (shouldn't) perform any async / blocking work here. + * + * @type {import('myst-common').DirectiveSpec} + */ +const mystTemplate = { + name: 'myst:template', + options: { + kind: { + type: String, + }, + 'full-title': { + type: Boolean, + }, + 'heading-depth': { + type: Number, + }, + }, + arg: { + type: String, + required: true, + }, + run(data) { + /** @type {MySTTemplateRef} */ + const templateRef = u( + 'myst-template-ref', + { + template: data.arg, + kind: data.options?.kind ?? 'site', + fullTitle: data.options?.['fullTitle'] ?? false, + headingDepth: data.options?.['heading-depth'] ?? 2, + }, + [], + ); + return [templateRef]; + }, +}; + +let _promise = undefined; + +/** + * Determine a URL-friendly slug for a given template ID. + * + * @param id - template ID + */ +function slugify(id) { + return id.replaceAll('/', '-'); +} + +/** + * Return the MyST AST for a given template option declaration. + * + * @param template - template declaration + * @param option - option declaration + */ +function createOption(template, option) { + if (!option) { + return []; + } + + // Build a definitionTerm for the given template option + const def = [ + u('definitionTerm', { identifier: `template-${slugify(template.id)}-${slugify(option.id)}` }, [ + u('strong', [u('text', option.id)]), + ...(option.type + ? [ + u('text', ' ('), + u('emphasis', [u('text', `${option.type}${option.required ? ', required' : ''}`)]), + u('text', ')'), + ] + : []), + ]), + ]; + + // Build a definitionDescription for the given template option, falling back on default text if + // no description is defined. + def.push( + u( + 'definitionDescription', + // Parse the description as MyST (if given) + option.description ? mystParse(option.description).children : [u('text', 'No description')], + ), + ); + return def; +} + +/** + * Load a MyST Template e.g. https://api.mystmd.org/templates/site/myst/book-theme + * + * @param url - url to MyST template + */ +async function loadFromTemplateMeta(url) { + const response = await fetch(url); + return await response.json(); +} + +/** + * Load a list of MyST templates with a given kind, e.g. https://api.mystmd.org/templates/site/ + * + * @param url - url to MyST templates + */ +async function loadByTemplateKind(url) { + const response = await fetch(url); + const { items } = await response.json(); + return await Promise.all(items.map((item) => loadFromTemplateMeta(item.links.self))); +} + +/** + * Load a list of all MyST templates given by api.mystmd.org + */ +async function loadTemplates() { + // Load top-level list of templates + const response = await fetch(`https://api.mystmd.org/templates/`); + const { links } = await response.json(); + // Load all the top-level kinds + return (await Promise.all(Object.values(links).map(loadByTemplateKind))).flat(); +} + +// Define some regular expressions to identify partial template names (e.g. book-theme) +// vs full names (e.g. site/myst/book-theme) +const PARTIAL_TEMPLATE_REGEX = /^[a-zA-Z0-9_-]+$/; +const TEMPLATE_REGEX = /^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+$/; +const FULL_TEMPLATE_REGEX = /^(site|tex|typst|docx)\/([a-zA-Z0-9_-]+)\/([a-zA-Z0-9_-]+)$/; + +/** + * MyST transform to fetch information about MyST templates from api.mystmd.org, and + * populate the children of myst-template-ref nodes using this data + * + * @param opts - (empty) options populated by the caller (MyST) + * @param utils - helpful utility functions + */ +function templateTransform(opts, utils) { + return async (mdast) => { + // This function is called during processing of all documents, with multiple invocations + // potentially running concurrently. To avoid fetching the templates for each call, + // we first create the promise (but _do not await it_) so that other invocations + // can await the result. + if (_promise === undefined) { + _promise = loadTemplates(); + } + + // Now we await the list of templates. After this promise has been resolved, this will + // happen instantly + let templates; + try { + templates = await _promise; + } catch (err) { + throw new Error('Error loading template information from https://api.mystmd.org'); + } + + // Using unist-util-select, a utility for walking unist (to which MyST confirms) graphs. + // We are looking for nodes of type `myst-template-ref`, which are created by our directive above + utils.selectAll('myst-template-ref', mdast).forEach((node) => { + // Figure out whether the caller gave a full template or partial template name. + // If the name is partial, we will try to resolve it into a full name. + const templateName = node.template; + let resolvedTemplateName; + if (templateName.match(PARTIAL_TEMPLATE_REGEX) && node.kind !== undefined) { + resolvedTemplateName = `${node.kind}/myst/${templateName}`; + } else if (templateName.match(TEMPLATE_REGEX) && node.kind !== undefined) { + resolvedTemplateName = `${node.kind}/${templateName}`; + } else if (templateName.match(FULL_TEMPLATE_REGEX)) { + resolvedTemplateName = templateName; + } else { + throw new Error(`Could not determine full name for template: ${templateName}`); + } + + // Let's now find the template information for the requested template name + const template = templates.find((template) => template.id === resolvedTemplateName); + if (template === undefined) { + throw new Error(`Could not find template ${templateName}`); + } + + // Parse the template name into useful parts + const [_, kind, namespace, name, ...rest] = template.id.match(FULL_TEMPLATE_REGEX); + + // Build the title node + const title = node.fullTitle ? template.id : name; + const slug = slugify(template.id); + const heading = u('heading', { depth: node.headingDepth, identifier: `template-${slug}` }, [ + u('inlineCode', title), + u('text', ' template'), + ]); + + // Parse the template description + const doc = template.description ? mystParse(template.description).children : []; + + // Build a definitionList of template options + const options = (template.options ?? {}) + .map((option) => createOption(template, option)) + .flat(); + const list = u('definitionList', [ + u('definitionTerm', { identifier: `template-${slug}-opts` }, [ + u('strong', [u('text', 'Options')]), + ]), + options.length > 0 + ? u('definitionDescription', [u('definitionList', options)]) + : u('definitionDescription', [u('text', 'No options')]), + ]); + + // Add a footer that links to the template source + const link = { + type: 'link', + url: template.links.source, + children: [ + { + type: 'text', + value: 'Source', + }, + ], + }; + + // Update the `myst-template-ref` children with our generated nodes + node.children = [heading, ...doc, list, link]; + }); + }; +} + +// Declare a transform plugin +const mystTemplateTransform = { + plugin: templateTransform, + stage: 'document', +}; + +/** + * @type {import('myst-common').MystPlugin} + */ +const plugin = { + name: 'MyST Template Documentation Plugins', + author: 'Angus Hollands', + license: 'MIT', + directives: [mystTemplate], + roles: [], + transforms: [mystTemplateTransform], +}; + +export default plugin; diff --git a/docs/website-templates.md b/docs/website-templates.md index f658bceb1..a824c1678 100644 --- a/docs/website-templates.md +++ b/docs/website-templates.md @@ -1,9 +1,19 @@ --- -title: Website Templates +title: Website Themes & Templates description: There are two templates for MyST websites, a `book-theme`, based loosely on JupyterBook, and an `article-theme` that is designed for scientific documents with supporting notebooks. --- -There are currently two templates for MyST websites, a `book-theme`, which is the default and is based loosely on JupyterBook and an `article-theme` that is designed for scientific documents with supporting notebooks. The documentation for this site is using the `book-theme`, for a demonstration of the `article-theme`, you can see [an article on finite volume](https://simpeg.xyz/tle-finitevolume). +Web templates allow MyST to render documents as HTML-based sites. +These provide different reading experiences that are designed for different types of MyST documents. +They are defined via the same templating system used for [static document exporting](./documents-exports.md), and a base set of web themes can be found in the [`executablebooks/myst-themes` repository](https://github.com/executablebooks/myst-theme/tree/main/themes). + +:::{tip} Themes and templates mean the same thing +For the remainder of this page, assume that "theme" and "template" mean the same thing. +::: + +## Themes bundled with MyST + +There are two templates for MyST websites, a `book-theme`, which is the default and is based loosely on JupyterBook and an `article-theme` that is designed for scientific documents with supporting notebooks. The documentation for this site uses the `book-theme`. For a demonstration of the `article-theme`, you can see [an article on finite volume](https://simpeg.xyz/tle-finitevolume). :::::{tab-set} ::::{tab} Article Theme @@ -19,9 +29,15 @@ Example of a site using the `book-theme`, ([online](https://mystmd.org), [source :::: ::::: -## Changing Site Templates +### Article Theme + +The article theme is centered around a single document with supporting content, which is how many scientific articles are structured today: a narrative article with associated computational notebooks to reproduce a figure, document data-cleaning steps, or provide interactive visualization. These are listed as "supporting documents" in this theme and can be pulled in as normal with your [](./table-of-contents.md). For information on how to import your figures into your article, see [](./reuse-jupyter-outputs.md). + +The frontmatter that is displayed at the top of the article is the contents of your project, including a project [thumbnail and banner](#thumbnail-and-banner). The affiliations for your authors, their ORCID, email, etc. are available by clicking directly on the author name. + +## Change Site Templates -To change your website template from the default (`book-theme`), use the `site: template:` property: +To manually specify your website template, use the `site.template` property: ```{code} yaml :filename: myst.yml @@ -34,17 +50,61 @@ site: template: article-theme ``` -### Article Theme +(site-navigation)= +## Site navigation -The article theme is centered around a single document with supporting content, which is how many scientific articles are structured today: a narrative article with associated computational notebooks to reproduce a figure, document data-cleaning steps, or provide interactive visualization. These are listed as "supporting documents" in this theme and can be pulled in as normal with your [](./table-of-contents.md). For information on how to import your figures into your article, see [](./reuse-jupyter-outputs.md). +In addition to [your MyST document's Table of Contents](./table-of-contents.md), you may specify a top-level navigation for your MyST site. +These links are displayed across all pages of your site, and are useful for quickly jumping to sections of your site, or for external links. -The frontmatter that is displayed at the top of the article is the contents of your project, including a project [thumbnail and banner](#thumbnail-and-banner). The affiliations for your authors, their ORCID, email, etc. are available by clicking directly on the author name. +Specify top-level navigation with the `site.nav` option, and provide a collection of navigation links similar to [how the Table of Contents is structured](./table-of-contents.md). For example: + +```{code-block} yaml +:filename: myst.yml + +site: + nav: + # A top-level dropdown + - title: Dropdown links + children: + - title: Page one + url: https://mystmd.org + - title: Page two + url: https://mystmd.org/guide + # A top-level link + - title: A standalone link + url: https://jupyter.org +``` + +% TODO: Clarify why some things have their own section (nav: and actions:) while +% others are nested under site.options. +## Action buttons + +Action buttons provide a more noticeable button that invites users to click on them. +They are located in the top-right of the page. + +Add action buttons to your site header with the `site.actions` option. For example: + +```{code-block} yaml +:filename: myst.yml + +site: + actions: + - title: Button text + url: https://mystmd.org + - title: Second button text + url: https://mystmd.org/guide +``` (site-options)= ## Site Options -There are a number of common options between the site templates. These should be placed in the `site.options` in your `myst.yml`. +Site options allow you to configure a theme's behavior.[^opts] +These should be placed in the `site.options` in your `myst.yml`. +For example: + +[^opts]: They are generally unique to the theme (and thus in a dediated `site.options` key rather than a top-level option in `site`). + ```{code-block} yaml :filename: myst.yml @@ -54,21 +114,25 @@ site: logo: my-site-logo.svg ``` -```{list-table} Site Options -:header-rows: 1 -:label: tbl:site-options -* - option - - description -* - `favicon` - - a file - Local path to favicon image -* - `logo` - - a file - Local path to logo image -* - `logo_dark` - - a file - Local path to logo image to be used in dark mode only -* - `logo_text` - - a string - Short text to display next to logo at the top of all pages -* - `analytics_google` - - a string - Google analytics key, see [](./analytics.md) -* - `analytics_plausible` - - a string - Plausible analytics key, see [](./analytics.md) +Below is a table of options for each theme bundled with MyST. + +```{myst:template} book-theme +:heading-depth: 3 ``` + + +```{myst:template} article-theme +:heading-depth: 3 +``` + +## Other top-level site configuration + +There are some other top-level site configuration options not documented here. +You can find them in the following two files. + +% TODO: Add proper documentation for these +% ref: https://github.com/executablebooks/mystmd/issues/1211 +https://github.com/executablebooks/mystmd/blob/8e7ac4ae05d833140181ed69aa1e354face7caa0/packages/myst-frontmatter/src/site/types.ts#L57-L83 + + +https://github.com/executablebooks/mystmd/blob/main/packages/myst-config/src/site/types.ts?rgh-link-date=2024-05-15T06%3A31%3A26Z#L26-L33