Skip to content

Commit

Permalink
feat: Support section-end mark (line with only hashes) (#175)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
MurakamiShinyu committed Oct 13, 2023
1 parent 76721d2 commit 63869ed
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 9 deletions.
2 changes: 2 additions & 0 deletions docs/ja/vfm.md
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,8 @@ ruby rt {
- 見出しの行が `#` ではじまり同数以上の `#` で終わる場合はセクションを分けません
- `### Not Sectionize ###` (同じ数の `#` で囲まれている) -- セクション分けしない
- `### Sectionize ##` (閉じの `#` の数が足りない) -- セクション分けする
- `#` だけからなる行により `#` の数と一致する深さのセクションを終了させることができる
- 例: `### Heading 3` で開始したセクションは `###` で終了させられる
- 親が `blockquote` の場合はセクションを分けません
- 見出しの深さへ一致するように、セクションの `levelN` クラスを設定します
- 見出しの `id` 属性値をセクションの `aria-labelledby` 属性へ値をコピーします
Expand Down
2 changes: 2 additions & 0 deletions docs/vfm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
39 changes: 30 additions & 9 deletions src/plugins/section.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,37 @@ 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.
* @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;
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;
};

/**
Expand Down Expand Up @@ -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,
);
};

/**
Expand Down
69 changes: 69 additions & 0 deletions tests/section.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<section class="level1" aria-labelledby="heading-1">
<h1 id="heading-1">Heading 1</h1>
<p>Depth 1</p>
<section class="level2" aria-labelledby="heading-2">
<h2 id="heading-2">Heading 2</h2>
<p>Depth 2</p>
<section class="level3" aria-labelledby="heading-3">
<h3 id="heading-3">Heading 3</h3>
<p>Depth 3</p>
<section class="level4" aria-labelledby="heading-4">
<h4 id="heading-4">Heading 4</h4>
<p>Depth 4</p>
<section class="level5" aria-labelledby="heading-5">
<h5 id="heading-5">Heading 5</h5>
<p>Depth 5</p>
<section class="level6" aria-labelledby="heading-6">
<h6 id="heading-6">Heading 6</h6>
<p>Depth 6</p>
</section>
<p>Depth 5 again</p>
</section>
</section>
<p>Depth 3 again</p>
</section>
</section>
<p>Depth 1 again</p>
</section>
`;
expect(received).toBe(expected);
});

0 comments on commit 63869ed

Please sign in to comment.