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: Added section leveling #90

Merged
merged 3 commits into from
Apr 28, 2021
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
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,
);
}
};
14 changes: 7 additions & 7 deletions tests/attr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { stripIndent } from 'common-tags';
import { buildProcessorTestingCode } from './utils';

it(
'Header with attributes',
'Heading with attributes',
buildProcessorTestingCode(
`# Heading {#foo}`,
stripIndent`
Expand All @@ -12,12 +12,12 @@ it(
│ data: {"hProperties":{"id":"foo"}}
└─0 text "Heading"
`,
`<section id="foo"><h1>Heading</h1></section>`,
`<section id="foo" class="level1"><h1>Heading</h1></section>`,
),
);

it(
'Header with attributes, specification by line break',
'Heading with attributes, specification by line break',
buildProcessorTestingCode(
`# Heading\n{#foo}`,
stripIndent`
Expand All @@ -27,12 +27,12 @@ it(
│ data: {"hProperties":{"id":"foo"}}
└─0 text "Heading"
`,
`<section id="foo"><h1>Heading</h1></section>`,
`<section id="foo" class="level1"><h1>Heading</h1></section>`,
),
);

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`
Expand All @@ -44,15 +44,15 @@ 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>`,
),
);

// `remark-attr` needs to be fixed
// 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`
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
2 changes: 1 addition & 1 deletion tests/math.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ $$</p>`;
expect(received).toBe(expected);
});

it('HTML header and body', () => {
it('HTML head and body', () => {
const received = stringify('$x=y$', { math: true, disableFormatHtml: true });
const expected = `<!doctype html>
<html>
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 = `# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6`;
const received = stringify(md, { partial: true });
const expected = `
<section id="heading-1" class="level1">
<h1>Heading 1</h1>
<section id="heading-2" class="level2">
<h2>Heading 2</h2>
<section id="heading-3" class="level3">
<h3>Heading 3</h3>
<section id="heading-4" class="level4">
<h4>Heading 4</h4>
<section id="heading-5" class="level5">
<h5>Heading 5</h5>
<section id="heading-6" class="level6">
<h6>Heading 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 = `# 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 = `
<section class="level1 depth1" id="heading-1">
<h1>Heading 1</h1>
<section class="level2 depth2" id="heading-2">
<h2>Heading 2</h2>
<section class="level3 depth3" id="heading-3">
<h3>Heading 3</h3>
<section class="level4 depth4" id="heading-4">
<h4>Heading 4</h4>
<section class="level5 depth5" id="heading-5">
<h5>Heading 5</h5>
<section class="level6 depth6" id="heading-6">
<h6>Heading 6</h6>
</section>
</section>
</section>
</section>
</section>
</section>
`;
expect(received).toBe(expected);
});

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