Skip to content

Commit

Permalink
feat: Support non-sectionize headings, enclosed by equal number of ha…
Browse files Browse the repository at this point in the history
…shes (#172)

This add the following heading auto-sectioning rule:

- Do not sectionize if the heading line starts with `#`s and ends with equal or greater number of `#`s.
  - `### Not Sectionize ###` (enclosed by equal number of `#`s) -- not sectionize
  - `### Sectionize ##` (insufficient number of closing `#`s) -- sectionize

resolves #155
  • Loading branch information
MurakamiShinyu authored Oct 12, 2023
1 parent 5a18123 commit 463c615
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 31 deletions.
3 changes: 3 additions & 0 deletions docs/ja/vfm.md
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,9 @@ ruby rt {

見出しを階層的なセクションにします。

- 見出しの行が `#` ではじまり同数以上の `#` で終わる場合はセクションを分けません
- `### Not Sectionize ###` (同じ数の `#` で囲まれている) -- セクション分けしない
- `### Sectionize ##` (閉じの `#` の数が足りない) -- セクション分けする
- 親が `blockquote` の場合はセクションを分けません
- 見出しの深さへ一致するように、セクションの `levelN` クラスを設定します
- 見出しの `id` 属性値をセクションの `aria-labelledby` 属性へ値をコピーします
Expand Down
3 changes: 3 additions & 0 deletions docs/vfm.md
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,9 @@ If want to escape the delimiter pipe `|`, add `\` immediately before it.

Make the heading a hierarchical section.

- Do not sectionize if the heading line starts with `#`s and ends with equal or greater number of `#`s.
- `### Not Sectionize ###` (enclosed by equal number of `#`s) -- not sectionize
- `### Sectionize ##` (insufficient number of closing `#`s) -- sectionize
- Do not sectionize if parent is `blockquote`.
- Set the `levelN` class in the section to match the heading depth.
- Copy the value of the `id` attribute of the heading to the `aria-labelledby` attribute of the section.
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export function VFM(

const processor = unified()
.use(markdown(hardLineBreaks, math))
.data('settings', { position: false })
.data('settings', { position: true })
.use(html);

if (replace) {
Expand Down
28 changes: 26 additions & 2 deletions src/plugins/section.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import { Parent } from 'mdast';
import { VFile } from 'vfile';
import findAfter from 'unist-util-find-after';
import visit from 'unist-util-visit-parents';

Expand All @@ -31,6 +32,23 @@ const createProperties = (depth: number, node: any): KeyValue => {
return properties;
};

/**
* Check if the heading has a non-section mark (sufficient number of closing hashes).
* @param node Node of Markdown AST.
* @param file Virtual file.
* @returns `true` if the node has a non-section mark.
*/
const hasNonSectionMark = (node: any, file: VFile): boolean => {
const startOffset = node.position?.start.offset ?? 0;
const endOffset = node.position?.end.offset ?? 0;
const text = file.toString().slice(startOffset, endOffset);
const depth = node.depth;
if ((/[ \t](#+)$/.exec(text)?.[1]?.length ?? 0) >= depth) {
return true;
}
return false;
};

/**
* Wrap the header in sections.
* - Do not sectionize if parent is `blockquote`.
Expand All @@ -39,7 +57,10 @@ const createProperties = (depth: number, node: any): KeyValue => {
* @param ancestors Parents.
* @todo handle `@subtitle` properly.
*/
const sectionize = (node: any, ancestors: Parent[]) => {
const sectionizeIfRequired = (node: any, ancestors: Parent[], file: VFile) => {
if (hasNonSectionMark(node, file)) {
return;
}
const parent = ancestors[ancestors.length - 1];
if (parent.type === 'blockquote') {
return;
Expand Down Expand Up @@ -96,7 +117,10 @@ const sectionize = (node: any, ancestors: Parent[]) => {
* Process Markdown AST.
* @returns Transformer.
*/
export const mdast = () => (tree: any) => {
export const mdast = () => (tree: any, file: VFile) => {
const sectionize = (node: Node, ancestors: Parent[]) => {
sectionizeIfRequired(node, ancestors, file);
};
for (let depth = MAX_HEADING_DEPTH; depth > 0; depth--) {
visit(
tree,
Expand Down
20 changes: 20 additions & 0 deletions tests/section.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,26 @@ it('Disable section with blockquote heading', () => {
expect(received).toBe(expected);
});

it('Disable section with closing hashes', () => {
const md = '### Not Sectionize ###';
const received = stringify(md, { partial: true });
const expected = `
<h3 id="not-sectionize">Not Sectionize</h3>
`;
expect(received).toBe(expected);
});

it('Do not disable section with insufficient closing hashes', () => {
const md = '### Sectionize ##';
const received = stringify(md, { partial: true });
const expected = `
<section class="level3" aria-labelledby="sectionize">
<h3 id="sectionize">Sectionize</h3>
</section>
`;
expect(received).toBe(expected);
});

it('<h7> is not heading', () => {
const md = '####### こんにちは {.test}';
const received = stringify(md, { partial: true, disableFormatHtml: true });
Expand Down
61 changes: 33 additions & 28 deletions tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,36 @@ import { StringifyMarkdownOptions, VFM } from '../src';
* @param expectedHtml Expected HTML string.
* @param options Option for convert Markdown to VFM (HTML).
*/
export const buildProcessorTestingCode = (
input: string,
expectedMdast: string,
expectedHtml: string,
{
style = undefined,
partial = true,
title = undefined,
language = undefined,
replace = undefined,
hardLineBreaks = false,
disableFormatHtml = true,
math = false,
}: StringifyMarkdownOptions = {},
) => (): any => {
const vfm = VFM({
style,
partial,
title,
language,
replace,
hardLineBreaks,
disableFormatHtml,
math,
}).freeze();
expect(unistInspect.noColor(vfm.parse(input))).toBe(expectedMdast.trim());
expect(String(vfm.processSync(input))).toBe(expectedHtml);
};
export const buildProcessorTestingCode =
(
input: string,
expectedMdast: string,
expectedHtml: string,
{
style = undefined,
partial = true,
title = undefined,
language = undefined,
replace = undefined,
hardLineBreaks = false,
disableFormatHtml = true,
math = false,
}: StringifyMarkdownOptions = {},
) =>
(): any => {
const vfm = VFM({
style,
partial,
title,
language,
replace,
hardLineBreaks,
disableFormatHtml,
math,
}).freeze();
const R = / \(.+?\)$/gm; // Remove position information
expect(unistInspect.noColor(vfm.parse(input)).replace(R, '')).toBe(
expectedMdast.trim(),
);
expect(String(vfm.processSync(input))).toBe(expectedHtml);
};

0 comments on commit 463c615

Please sign in to comment.