From 63869eddb6a381c59a25ca9528ff0fd55ad962c4 Mon Sep 17 00:00:00 2001 From: Shinyu Murakami Date: Fri, 13 Oct 2023 18:59:44 +0900 Subject: [PATCH] feat: Support section-end mark (line with only hashes) (#175) This adds the following rule on sectionization: - A line with only `#`s can be used to end the section whose depth matches the number of the `#`s. - e.g., the section starting with `### Heading 3` can end with `###`. (related to #155, thanks @nosuke23 for the suggestion) --- docs/ja/vfm.md | 2 ++ docs/vfm.md | 2 ++ src/plugins/section.ts | 39 ++++++++++++++++++------ tests/section.test.ts | 69 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 9 deletions(-) diff --git a/docs/ja/vfm.md b/docs/ja/vfm.md index e4f6583..fea8d65 100644 --- a/docs/ja/vfm.md +++ b/docs/ja/vfm.md @@ -567,6 +567,8 @@ ruby rt { - 見出しの行が `#` ではじまり同数以上の `#` で終わる場合はセクションを分けません - `### Not Sectionize ###` (同じ数の `#` で囲まれている) -- セクション分けしない - `### Sectionize ##` (閉じの `#` の数が足りない) -- セクション分けする +- `#` だけからなる行により `#` の数と一致する深さのセクションを終了させることができる + - 例: `### Heading 3` で開始したセクションは `###` で終了させられる - 親が `blockquote` の場合はセクションを分けません - 見出しの深さへ一致するように、セクションの `levelN` クラスを設定します - 見出しの `id` 属性値をセクションの `aria-labelledby` 属性へ値をコピーします diff --git a/docs/vfm.md b/docs/vfm.md index 05eb44e..c9ab7ed 100644 --- a/docs/vfm.md +++ b/docs/vfm.md @@ -567,6 +567,8 @@ 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 +- A line with only `#`s can be used to end the section whose depth matches the number of the `#`s. + - e.g., the section starting with `### Heading 3` can end with `###`. - 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. diff --git a/src/plugins/section.ts b/src/plugins/section.ts index d53fa9e..77b4f64 100644 --- a/src/plugins/section.ts +++ b/src/plugins/section.ts @@ -32,6 +32,16 @@ const createProperties = (depth: number, node: any): KeyValue => { return properties; }; +const getHeadingLine = (node: any, file: VFile): string => { + if (node?.type !== 'heading') { + return ''; + } + const startOffset = node.position?.start.offset ?? 0; + const endOffset = node.position?.end.offset ?? 0; + const text = file.toString().slice(startOffset, endOffset); + return text.trim(); +}; + /** * Check if the heading has a non-section mark (sufficient number of closing hashes). * @param node Node of Markdown AST. @@ -39,14 +49,20 @@ const createProperties = (depth: number, node: any): KeyValue => { * @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; + const line = getHeadingLine(node, file); + return ( + !!line && (/^#.*[ \t](#+)$/.exec(line)?.[1]?.length ?? 0) >= node.depth + ); +}; + +/** + * Check if the node is a section-end mark (line with only hashes). + * @param node Node of Markdown AST. + * @returns `true` if the node is a section-end mark. + */ +const isSectionEndMark = (node: any, file: VFile): boolean => { + const line = getHeadingLine(node, file); + return !!line && /^(#+)$/.exec(line)?.[1]?.length === node.depth; }; /** @@ -131,7 +147,12 @@ const sectionizeIfRequired = (node: any, ancestors: Parent[], file: VFile) => { children: between, } as any; - parent.children.splice(startIndex, section.children.length, section); + parent.children.splice( + startIndex, + section.children.length + + (isSectionEndMark(end, file) && end.depth === depth ? 1 : 0), + section, + ); }; /** diff --git a/tests/section.test.ts b/tests/section.test.ts index aed68ef..e1c60f4 100644 --- a/tests/section.test.ts +++ b/tests/section.test.ts @@ -219,3 +219,72 @@ This is a note. `; expect(received).toBe(expected); }); + +it('Section-end marks', () => { + const md = `# Heading 1 + +Depth 1 + +## Heading 2 + +Depth 2 + +### Heading 3 + +Depth 3 + +#### Heading 4 + +Depth 4 + +##### Heading 5 + +Depth 5 + +###### Heading 6 + +Depth 6 + +###### + +Depth 5 again + +#### + +Depth 3 again + +## + +Depth 1 again`; + const received = stringify(md, { partial: true }); + const expected = ` +
+

Heading 1

+

Depth 1

+
+

Heading 2

+

Depth 2

+
+

Heading 3

+

Depth 3

+
+

Heading 4

+

Depth 4

+
+
Heading 5
+

Depth 5

+
+
Heading 6
+

Depth 6

+
+

Depth 5 again

+
+
+

Depth 3 again

+
+
+

Depth 1 again

+
+`; + expect(received).toBe(expected); +});