Skip to content

Commit

Permalink
feat: Added section leveling
Browse files Browse the repository at this point in the history
  • Loading branch information
akabekobeko committed Apr 28, 2021
1 parent 5fdcaa6 commit 021b72f
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 68 deletions.
22 changes: 19 additions & 3 deletions docs/vfm.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,22 +203,33 @@ ruby rt {
# Introduction {#intro}

# Welcome {.title}

# Level 1

## Level 2
```

**HTML**

```html
<section id="plain">
<section id="plain" class="level1">
<h1>Plain</h1>
</section>

<section id="intro">
<section id="intro" class="level1">
<h1>Introduction</h1>
</section>

<section id="welcome" class="title">
<section class="level1 title" id="welcome">
<h1>Welcome</h1>
</section>

<section id="level-1" class="level1">
<h1>Level 1</h1>
<section id="level-2" class="level2">
<h2>Level 2</h2>
</section>
</section>
```

**CSS**
Expand All @@ -234,6 +245,11 @@ section.title {

section.title > h1:first-child {
}

.level1 {
}
.level2 {
}
```

### Plain section
Expand Down
57 changes: 38 additions & 19 deletions src/plugins/section.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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: {
Expand All @@ -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,
);
}
};
6 changes: 3 additions & 3 deletions tests/attr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ it(
│ data: {"hProperties":{"id":"foo"}}
└─0 text "Heading"
`,
`<section id="foo"><h1>Heading</h1></section>`,
`<section id="foo" class="level1"><h1>Heading</h1></section>`,
),
);

Expand All @@ -27,7 +27,7 @@ it(
│ data: {"hProperties":{"id":"foo"}}
└─0 text "Heading"
`,
`<section id="foo"><h1>Heading</h1></section>`,
`<section id="foo" class="level1"><h1>Heading</h1></section>`,
),
);

Expand All @@ -44,7 +44,7 @@ it(
└─1 emphasis[1]
└─0 text "test"
`,
`<section id="foo"><h1>Heading <em>test</em></h1></section>`,
`<section id="foo" class="level1"><h1>Heading <em>test</em></h1></section>`,
),
);

Expand Down
40 changes: 0 additions & 40 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -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 `<body>` part.
* @param hardLineBreaks Add `<br>` 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(`<section class="ok"></section>`);
});

it('stringify markdown string into html document', () => {
expect(lib.stringify('# こんにちは', { disableFormatHtml: true }))
.toBe(`<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>こんにちは</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<section id="こんにちは"><h1>こんにちは</h1></section>
</body>
</html>
`);
});

it('replace', () => {
const rules = [
{
Expand Down
6 changes: 3 additions & 3 deletions tests/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class: 'my-class'
<meta name="author" content="Author">
</head>
<body class="my-class">
<section id="page-title">
<section id="page-title" class="level1">
<h1>Page Title</h1>
</section>
</body>
Expand All @@ -39,7 +39,7 @@ it('title from heading, missing "title" property of Frontmatter', () => {
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<section id="page-title">
<section id="page-title" class="level1">
<h1>Page Title</h1>
</section>
</body>
Expand Down Expand Up @@ -142,7 +142,7 @@ class: 'my-class'
<meta name="author" content="Author">
</head>
<body class="my-class">
<section id="heading-title">
<section id="heading-title" class="level1">
<h1>Heading Title</h1>
</section>
</body>
Expand Down
108 changes: 108 additions & 0 deletions tests/section.test.ts
Original file line number Diff line number Diff line change
@@ -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 = '<section class="level1 ok"></section>';
expect(received).toBe(expected);
});
*/

it('<h1>', () => {
const md = '# こんにちは {.test}';
const received = stringify(md, { partial: true, disableFormatHtml: true });
const expected =
'<section class="level1 test" id="こんにちは"><h1>こんにちは</h1></section>';
expect(received).toBe(expected);
});

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

it('<h1>, ... <h6>', () => {
const md = `# Header 1
## Header 2
### Header 3
#### Header 4
##### Header 5
###### Header 6`;
const received = stringify(md, { partial: true });
const expected = `
<section id="header-1" class="level1">
<h1>Header 1</h1>
<section id="header-2" class="level2">
<h2>Header 2</h2>
<section id="header-3" class="level3">
<h3>Header 3</h3>
<section id="header-4" class="level4">
<h4>Header 4</h4>
<section id="header-5" class="level5">
<h5>Header 5</h5>
<section id="header-6" class="level6">
<h6>Header 6</h6>
</section>
</section>
</section>
</section>
</section>
</section>
`;
expect(received).toBe(expected);
});

// It seems that when the class is processed by `remark-attr`, it is output before id.
it('<h1>, ... <h6> 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 = `
<section class="level1 depth1" id="header-1">
<h1>Header 1</h1>
<section class="level2 depth2" id="header-2">
<h2>Header 2</h2>
<section class="level3 depth3" id="header-3">
<h3>Header 3</h3>
<section class="level4 depth4" id="header-4">
<h4>Header 4</h4>
<section class="level5 depth5" id="header-5">
<h5>Header 5</h5>
<section class="level6 depth6" id="header-6">
<h6>Header 6</h6>
</section>
</section>
</section>
</section>
</section>
</section>
`;
expect(received).toBe(expected);
});

it('Complex structure', () => {
const md = `# Header 1
## Header 2 {.foo}
# Header 1`;
const received = stringify(md, { partial: true });
const expected = `
<section id="header-1" class="level1">
<h1>Header 1</h1>
<section class="level2 foo" id="header-2">
<h2>Header 2</h2>
</section>
</section>
<section id="header-1-1" class="level1">
<h1>Header 1</h1>
</section>
`;
expect(received).toBe(expected);
});

0 comments on commit 021b72f

Please sign in to comment.