From 021b72ffc2650499682904715da80932ffb977fc Mon Sep 17 00:00:00 2001 From: akabeko Date: Wed, 28 Apr 2021 18:03:03 +0900 Subject: [PATCH 1/3] feat: Added section leveling --- docs/vfm.md | 22 +++++++-- src/plugins/section.ts | 57 ++++++++++++++-------- tests/attr.test.ts | 6 +-- tests/index.test.ts | 40 --------------- tests/metadata.test.ts | 6 +-- tests/section.test.ts | 108 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 171 insertions(+), 68 deletions(-) create mode 100644 tests/section.test.ts diff --git a/docs/vfm.md b/docs/vfm.md index 6645a08..4e0d3f7 100644 --- a/docs/vfm.md +++ b/docs/vfm.md @@ -203,22 +203,33 @@ ruby rt { # Introduction {#intro} # Welcome {.title} + +# Level 1 + +## Level 2 ``` **HTML** ```html -
+

Plain

-
+

Introduction

-
+

Welcome

+ +
+

Level 1

+
+

Level 2

+
+
``` **CSS** @@ -234,6 +245,11 @@ section.title { section.title > h1:first-child { } + +.level1 { +} +.level2 { +} ``` ### Plain section diff --git a/src/plugins/section.ts b/src/plugins/section.ts index 7cadbc2..0bf9c26 100644 --- a/src/plugins/section.ts +++ b/src/plugins/section.ts @@ -1,29 +1,24 @@ -// derived from remark-sectionize -// https://github.com/jake-low/remark-sectionize -// MIT License -// original: 2019 Jake Low -// modified: 2020 Yasuaki Uechi +/** + * derived from `remark-sectionize`. + * original: 2019 Jake Low + * modified: 2020 Yasuaki Uechi, 2021 and later is Akabeko + * @license MIT + * @see https://github.com/jake-low/remark-sectionize + */ import { Parent } from 'mdast'; import findAfter from 'unist-util-find-after'; import visit from 'unist-util-visit-parents'; -// TODO: handle @subtitle properly - +/** Maximum depth of hierarchy to process headings. */ const MAX_HEADING_DEPTH = 6; -export const mdast = () => (tree: any) => { - for (let depth = MAX_HEADING_DEPTH; depth > 0; depth--) { - visit( - tree, - (node: any) => { - return node.type === 'heading' && node.depth === depth; - }, - sectionize as any, - ); - } -}; - +/** + * Wrap the header in sections. + * @param node Node of Markdown AST. + * @param ancestors Parents. + * @todo handle `@subtitle` properly. + */ function sectionize(node: any, ancestors: Parent[]) { const start = node; const depth = start.depth; @@ -66,6 +61,14 @@ function sectionize(node: any, ancestors: Parent[]) { return; } + // output section levels like Pandoc + if (Array.isArray(hProperties.class)) { + // `remark-attr` may add classes, so make sure they come before them (always top) + hProperties.class.unshift(`level${depth}`); + } else { + hProperties.class = [`level${depth}`]; + } + const section = { type, data: { @@ -78,3 +81,19 @@ function sectionize(node: any, ancestors: Parent[]) { parent.children.splice(startIndex, section.children.length, section); } + +/** + * Process Markdown AST. + * @returns Transformer. + */ +export const mdast = () => (tree: any) => { + for (let depth = MAX_HEADING_DEPTH; depth > 0; depth--) { + visit( + tree, + (node: any) => { + return node.type === 'heading' && node.depth === depth; + }, + sectionize as any, + ); + } +}; diff --git a/tests/attr.test.ts b/tests/attr.test.ts index 9fa042a..c3ec3b8 100644 --- a/tests/attr.test.ts +++ b/tests/attr.test.ts @@ -12,7 +12,7 @@ it( │ data: {"hProperties":{"id":"foo"}} └─0 text "Heading" `, - `

Heading

`, + `

Heading

`, ), ); @@ -27,7 +27,7 @@ it( │ data: {"hProperties":{"id":"foo"}} └─0 text "Heading" `, - `

Heading

`, + `

Heading

`, ), ); @@ -44,7 +44,7 @@ it( └─1 emphasis[1] └─0 text "test" `, - `

Heading test

`, + `

Heading test

`, ), ); diff --git a/tests/index.test.ts b/tests/index.test.ts index 9867d20..2b4a2b8 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,46 +1,6 @@ import * as lib from '../src'; import { ReplaceRule } from '../src/plugins/replace'; -/** - * Run VFM stringify in partial mode. - * @param body Markdown string that becomes `` part. - * @param hardLineBreaks Add `
` at the position of hard line breaks, without needing spaces. - * @returns HTML string. - */ -function partial(body: string, hardLineBreaks = false) { - return lib.stringify(body, { - partial: true, - hardLineBreaks, - disableFormatHtml: true, - }); -} - -// Snippet -// -// it('do something', ()=>{ -// expect(partial(``)).toBe(``) -// }) - -it.skip('plain section', () => { - expect(partial(`# {.ok}`)).toBe(`
`); -}); - -it('stringify markdown string into html document', () => { - expect(lib.stringify('# こんにちは', { disableFormatHtml: true })) - .toBe(` - - - -こんにちは - - - -

こんにちは

- - -`); -}); - it('replace', () => { const rules = [ { diff --git a/tests/metadata.test.ts b/tests/metadata.test.ts index afd4003..8b7e23f 100644 --- a/tests/metadata.test.ts +++ b/tests/metadata.test.ts @@ -20,7 +20,7 @@ class: 'my-class' -
+

Page Title

@@ -39,7 +39,7 @@ it('title from heading, missing "title" property of Frontmatter', () => { -
+

Page Title

@@ -142,7 +142,7 @@ class: 'my-class' -
+

Heading Title

diff --git a/tests/section.test.ts b/tests/section.test.ts new file mode 100644 index 0000000..944f367 --- /dev/null +++ b/tests/section.test.ts @@ -0,0 +1,108 @@ +import { stringify } from '../src/index'; + +// This test always fails, `remark-attr` does not handle empty headings. +/* +it('plain section', () => { + const md = '# {.ok}'; + const received = stringify(md, { partial: true, disableFormatHtml: true }); + const expected = '
'; + expect(received).toBe(expected); +}); +*/ + +it('

', () => { + const md = '# こんにちは {.test}'; + const received = stringify(md, { partial: true, disableFormatHtml: true }); + const expected = + '

こんにちは

'; + expect(received).toBe(expected); +}); + +it(' is not heading', () => { + const md = '####### こんにちは {.test}'; + const received = stringify(md, { partial: true, disableFormatHtml: true }); + const expected = '

####### こんにちは {.test}

'; + expect(received).toBe(expected); +}); + +it('

, ...

', () => { + const md = `# Header 1 +## Header 2 +### Header 3 +#### Header 4 +##### Header 5 +###### Header 6`; + const received = stringify(md, { partial: true }); + const expected = ` +
+

Header 1

+
+

Header 2

+
+

Header 3

+
+

Header 4

+
+
Header 5
+
+
Header 6
+
+
+
+
+
+
+`; + expect(received).toBe(expected); +}); + +// It seems that when the class is processed by `remark-attr`, it is output before id. +it('

, ...

with attribute', () => { + const md = `# Header 1 {.depth1} +## Header 2 {.depth2} +### Header 3 {.depth3} +#### Header 4 {.depth4} +##### Header 5 {.depth5} +###### Header 6 {.depth6}`; + const received = stringify(md, { partial: true }); + const expected = ` +
+

Header 1

+
+

Header 2

+
+

Header 3

+
+

Header 4

+
+
Header 5
+
+
Header 6
+
+
+
+
+
+
+`; + expect(received).toBe(expected); +}); + +it('Complex structure', () => { + const md = `# Header 1 +## Header 2 {.foo} +# Header 1`; + const received = stringify(md, { partial: true }); + const expected = ` +
+

Header 1

+
+

Header 2

+
+
+
+

Header 1

+
+`; + expect(received).toBe(expected); +}); From 6bd8b3056b850d3f073df3ed3c685762dac54537 Mon Sep 17 00:00:00 2001 From: akabeko Date: Wed, 28 Apr 2021 19:49:01 +0900 Subject: [PATCH 2/3] docs: Changed heading description from "Header" to "Heading" --- tests/attr.test.ts | 8 ++-- tests/math.test.ts | 2 +- tests/section.test.ts | 90 +++++++++++++++++++++---------------------- 3 files changed, 50 insertions(+), 50 deletions(-) diff --git a/tests/attr.test.ts b/tests/attr.test.ts index c3ec3b8..9d11dac 100644 --- a/tests/attr.test.ts +++ b/tests/attr.test.ts @@ -2,7 +2,7 @@ import { stripIndent } from 'common-tags'; import { buildProcessorTestingCode } from './utils'; it( - 'Header with attributes', + 'Heading with attributes', buildProcessorTestingCode( `# Heading {#foo}`, stripIndent` @@ -17,7 +17,7 @@ it( ); it( - 'Header with attributes, specification by line break', + 'Heading with attributes, specification by line break', buildProcessorTestingCode( `# Heading\n{#foo}`, stripIndent` @@ -32,7 +32,7 @@ it( ); it( - 'Header with attributes and inline elements, specification by line break', + 'Heading with attributes and inline elements, specification by line break', buildProcessorTestingCode( `# Heading *test*\n{#foo}`, stripIndent` @@ -52,7 +52,7 @@ it( // https://github.com/arobase-che/remark-attr/issues/24 /* it( - 'Header with attributes and inline elements', + 'Heading with attributes and inline elements', buildProcessorTestingCode( `# Heading *test* {#foo}`, stripIndent` diff --git a/tests/math.test.ts b/tests/math.test.ts index 312f58e..067da12 100644 --- a/tests/math.test.ts +++ b/tests/math.test.ts @@ -149,7 +149,7 @@ $$

`; expect(received).toBe(expected); }); -it('HTML header and body', () => { +it('HTML heading and body', () => { const received = stringify('$x=y$', { math: true, disableFormatHtml: true }); const expected = ` diff --git a/tests/section.test.ts b/tests/section.test.ts index 944f367..fe9524f 100644 --- a/tests/section.test.ts +++ b/tests/section.test.ts @@ -26,26 +26,26 @@ it(' is not heading', () => { }); it('

, ...

', () => { - const md = `# Header 1 -## Header 2 -### Header 3 -#### Header 4 -##### Header 5 -###### Header 6`; + const md = `# Heading 1 +## Heading 2 +### Heading 3 +#### Heading 4 +##### Heading 5 +###### Heading 6`; const received = stringify(md, { partial: true }); const expected = ` -
-

Header 1

-
-

Header 2

-
-

Header 3

-
-

Header 4

-
-
Header 5
-
-
Header 6
+
+

Heading 1

+
+

Heading 2

+
+

Heading 3

+
+

Heading 4

+
+
Heading 5
+
+
Heading 6
@@ -58,26 +58,26 @@ it('

, ...

', () => { // It seems that when the class is processed by `remark-attr`, it is output before id. it('

, ...

with attribute', () => { - const md = `# Header 1 {.depth1} -## Header 2 {.depth2} -### Header 3 {.depth3} -#### Header 4 {.depth4} -##### Header 5 {.depth5} -###### Header 6 {.depth6}`; + const md = `# Heading 1 {.depth1} +## Heading 2 {.depth2} +### Heading 3 {.depth3} +#### Heading 4 {.depth4} +##### Heading 5 {.depth5} +###### Heading 6 {.depth6}`; const received = stringify(md, { partial: true }); const expected = ` -
-

Header 1

-
-

Header 2

-
-

Header 3

-
-

Header 4

-
-
Header 5
-
-
Header 6
+
+

Heading 1

+
+

Heading 2

+
+

Heading 3

+
+

Heading 4

+
+
Heading 5
+
+
Heading 6
@@ -89,19 +89,19 @@ it('

, ...

with attribute', () => { }); it('Complex structure', () => { - const md = `# Header 1 -## Header 2 {.foo} -# Header 1`; + const md = `# Heading 1 +## Heading 2 {.foo} +# Heading 1`; const received = stringify(md, { partial: true }); const expected = ` -
-

Header 1

-
-

Header 2

+
+

Heading 1

+
+

Heading 2

-
-

Header 1

+
+

Heading 1

`; expect(received).toBe(expected); From 659d6c3fe5899c8a6376fe8df9488abcb5f52fa3 Mon Sep 17 00:00:00 2001 From: akabeko Date: Wed, 28 Apr 2021 21:50:52 +0900 Subject: [PATCH 3/3] Update tests/math.test.ts Co-authored-by: Shinyu Murakami --- tests/math.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/math.test.ts b/tests/math.test.ts index 067da12..8883cbb 100644 --- a/tests/math.test.ts +++ b/tests/math.test.ts @@ -149,7 +149,7 @@ $$

`; expect(received).toBe(expected); }); -it('HTML heading and body', () => { +it('HTML head and body', () => { const received = stringify('$x=y$', { math: true, disableFormatHtml: true }); const expected = `