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

feat: Support non-sectionize headings, enclosed by equal number of hashes #172

Merged
merged 1 commit into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
};
Loading