Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

➕ Add myst:template directive for documentation #1209

Merged
merged 9 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/myst.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ project:
- directives.mjs
- unsplash.mjs
- latex.mjs
- templates.mjs
error_rules:
- rule: link-resolves
severity: ignore
Expand Down
34 changes: 34 additions & 0 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
::::
179 changes: 179 additions & 0 deletions docs/templates.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { u } from 'unist-builder';
import { mystParse } from 'myst-parser';

/**
* Create a documentation section for a directive
*
* @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, vfile) {
return [
u(
'myst-template-ref',
{
template: data.arg,
kind: data.options?.kind ?? 'site',
fullTitle: data.options?.['fullTitle'] ?? false,
headingDepth: data.options?.['heading-depth'] ?? 2,
},
[],
),
];
},
};

let _promise = undefined;


function slugify(id) {
return id.replaceAll('/', '-');
}

function createOption(template, option) {
if (!option) return [];
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', ')'),
]
: []),
]),
];

if (option.description) {
def.push(
u(
'definitionDescription',
option.description ? mystParse(option.description).children : [u('text', 'No description')],
),
);
}
return def;
}

async function loadFromTemplateMeta(url) {
const response = await fetch(url);
return await response.json();
}

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)));
}

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();
}

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_-]+)$/;

function templateTransform(opts, utils) {
return async (mdast) => {
if (_promise === undefined) {
_promise = loadTemplates();
}

let templates;
try {
templates = await _promise;
} catch (err) {
throw new Error('Error loading template information from https://api.mystmd.org');
}
Comment on lines +163 to +168
Copy link
Collaborator

@agoose77 agoose77 May 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure that all concurrent builds have access to this at the same time by setting the global promise before resolving it.


utils.selectAll('myst-template-ref', mdast).forEach((node) => {
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 find template with name ${templateName}`);
}

// Match the name
const template = templates.find((template) => template.id === resolvedTemplateName);
const slug = slugify(template.id);

const [_, kind, namespace, name, ...rest] = template.id.match(FULL_TEMPLATE_REGEX);
const title = node.fullTitle ? template.id : name;
const heading = u('heading', { depth: node.headingDepth, identifier: `template-${slug}` }, [
u('inlineCode', title),
u('text', ' template'),
]);

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')]),
]);
const doc = template.description ? mystParse(template.description).children : [];
const link = {
type: 'link',
url: template.links.source,
children: [
{
type: 'text',
value: 'Source',
},
],
};
node.children = [heading, ...doc, list, link];
});
};
}

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;
101 changes: 77 additions & 24 deletions docs/website-templates.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.

To change your website template from the default (`book-theme`), use the `site: template:` property:
## Change Site Templates

To manually specify your website template, use the `site.template` property:

```{code} yaml
:filename: myst.yml
Expand All @@ -34,17 +50,56 @@ 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`.
For example:

```{code-block} yaml
:filename: myst.yml
Expand All @@ -54,21 +109,19 @@ 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.

% TODO: Parse the output as markdown when this is resolved:
% ref: https://github.com/executablebooks/mystmd/issues/1026
% TODO: Figure out how to attach a label to each of these tables.
agoose77 marked this conversation as resolved.
Show resolved Hide resolved


```{myst:template} book-theme
:heading-depth: 3
```


```{myst:template} article-theme
:heading-depth: 3
```