-
I've been using Markdown for standards documents. One of the big things in those documents is what I've dubbed section numbers. For example these hypothetical standards regarding placeholder text: ## 3. No placeholder text
Placeholder text adds no meaningful information. Besides, you might forget to remove it.
### 3.1. No Lipsum
You MUST not use any text copied from lipsum.org
### 3.2. No foo bar
The words `foo`, `bar`, and friends MUST not be used. The section numbers here are The idea is to create a plugin for section number references inside the document. Take the following example: #### 3.1.2. Hello, world!
[something something standard]
## 4. Be polite
You MUST greet everyone as you greet the world as defined in section 3.1.2. I would love for the section number reference to 3.1.2 to be an anchor link to the section. The output would be something like: #### 3.1.2. Hello, world!
[something something standard]
## 4. Be polite
You MUST greet everyone as you greet the world as defined in section [3.1.2. Hello, world!](#312-hello-world). This would make the (often quite long) documents easier to navigate. It also adds the context of the section title in such a way that you can't forget to update it manually. Note: I would probably use this in combination with something like Remark Reference Links for that extra bit of raw Markdown readability. Bonus points: Optional Remark heading ID support. Giving headers with section numbers a header id like |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 5 replies
-
here’s a plugin that does numbers: https://github.com/micromark/common-markup-state-machine/blob/main/build/number-headings.js |
Beta Was this translation helpful? Give feedback.
-
I've been looking a bit at what I can do with
So far, it looks like this will work. Though I fear that I'll need quite a bit of code to get it working properly, so a plugin might still be a good idea at some point. I'm not sure if there is a list of off-the-shelf directives though 🤔 A quick search yielded nothing. Is there a 'proper' way of sharing reusable directives? Or am I talking about plugins again? It'll be a while before I'll be able to find the time to finish it up though. Once I create a working directive, I'll share it here. |
Beta Was this translation helpful? Give feedback.
-
I've been able to make a pretty nice implementation for this myself :) Since there is no directive store (or similar) that I know of, I'll share it here for posterity. ExampleInput: ## 1.2. My amazing section
Lorum ipsum...
## 1.3. Another section
This elaborates on :section[1.2.]. Output: ## 1.2. My amazing section
Lorum ipsum...
## 1.3. Another section
This elaborates on [1.2. My amazing section](#12-my-amazing-section). OptionsThe directive plugin takes the following options:
Implementation👉 Click to expand implementation 👈import log from 'loglevel';
import { Heading, Link, Root, Text } from 'mdast';
import { TextDirective } from 'mdast-util-directive/complex-types';
import { toMarkdown } from 'mdast-util-to-markdown';
import { Plugin } from 'unified';
import { visit } from 'unist-util-visit';
interface SectionReferenceOptions {
strict?: boolean;
}
export interface SectionHeading extends Heading {
number: string;
id: string;
}
/**
* Remark Directive plugin to support the `:section` directive.
*
* The section directive allows us to refer to heading by their section number.
*
* @example
* Input:
*
* ```markdown
* ## 1.2. My amazing section
*
* Lorum ipsum...
*
* ## 1.3. Another section
*
* This elaborates on :section[1.2.].
* ```
*
* Output:
*
* ```markdown
* ## 1.2. My amazing section
*
* Lorum ipsum...
*
* ## 1.3. Another section
*
* This elaborates on [1.2. My amazing section](#12-my-amazing-section).
* ```
*/
export const remarkDirectiveSectionReference: Plugin<[SectionReferenceOptions?], Root> =
(options: SectionReferenceOptions = {}) =>
(tree: Root) => {
const sectionHeadings = collectSectionHeadings(tree);
visit(tree, (node) => {
if (node.type === 'textDirective' && node.name === 'section') {
resolveSectionReference(node, sectionHeadings, options);
}
});
};
const resolveSectionReference = (
node: TextDirective,
headings: SectionHeading[],
options: SectionReferenceOptions
) => {
const label = node.children[0];
if (!label || label.type !== 'text') {
textDirectiveToMarkdown(node);
const err = new Error(
`Bad section reference. Section reference '${toMarkdown(node).trim()}' is missing ` +
`which section it refers to.`
);
if (options.strict) throw err;
return log.error(err.message);
}
let reference = label.value;
if (!reference.endsWith('.')) {
reference += '.';
}
const heading = headings.find((h) => h.number === reference);
if (heading === undefined) {
const err = new Error(
`Bad section reference. Could not find section with number '${reference}'.`
);
if (options.strict) throw err;
log.error(err.message);
return textDirectiveToMarkdown(node);
}
const output = node as unknown as Link;
output.type = 'link';
output.title = null;
output.url = '#' + heading.id;
output.children = heading.children
.map((child) => {
if (child.type !== 'link' && child.type !== 'linkReference') {
return child;
}
return child.children;
})
.flat();
output.position = undefined;
};
export const collectSectionHeadings = (tree: Root): SectionHeading[] => {
const headings: SectionHeading[] = [];
visit(tree, (node) => {
if (node.type !== 'heading') {
return;
}
const firstChild = node.children[0];
if (!firstChild || firstChild.type !== 'text') {
return;
}
const text = firstChild.value.trim();
const pattern = new RegExp(/^(\d+\.)+/);
const match = pattern.exec(text);
if (match === null) {
// Heading contains no section number
return;
}
const headingId: string = toMarkdown({
type: 'paragraph',
children: node.children
.map((child) => {
if (child.type === 'link' || child.type === 'linkReference') {
return child.children;
}
return child;
})
.flat(),
})
.trim()
.toLowerCase()
// Remove all special characters
.replace(/[^\w\- ]/g, '')
// Replace whitespace with `-`
.replace(/\s/g, '-');
headings.push({
number: match[0],
id: headingId,
...node,
});
});
return headings;
};
export const textDirectiveToMarkdown = (node: TextDirective) => {
const output = node as unknown as Text;
output.type = 'text';
output.value = ':' + node.name;
if (node.children && node.children.length > 0) {
const label = toMarkdown({
type: 'paragraph',
children: node.children,
}).trim();
output.value += `[${label}]`;
}
if (node.attributes && Object.keys(node.attributes).length > 0) {
const attrs = Object.entries(node.attributes)
.map(([key, val]) => `${key}="${val}"`)
.join(' ');
output.value += `{${attrs}}`;
}
}; |
Beta Was this translation helpful? Give feedback.
I've been able to make a pretty nice implementation for this myself :) Since there is no directive store (or similar) that I know of, I'll share it here for posterity.
Example
Input:
Output:
Options
The directive plugin takes the following options:
false
true
will throw an error when encountering a bad section reference.When
false
will log a warning instead.Implementation
👉 Click to expand …