Skip to content

Commit

Permalink
feat: custom attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
uetchy committed Jul 2, 2020
1 parent 4151325 commit dd74d39
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 75 deletions.
7 changes: 5 additions & 2 deletions package.json
Expand Up @@ -25,23 +25,26 @@
"rehype-document": "^5.1.0",
"rehype-katex": "^3.0.0",
"rehype-raw": "^4.0.2",
"rehype-slug": "^3.0.0",
"rehype-stringify": "^8.0.0",
"remark-attr": "^0.11.1",
"remark-breaks": "^1.0.5",
"remark-footnotes": "^1.0.0",
"remark-frontmatter": "^2.0.0",
"remark-math": "^2.0.1",
"remark-parse": "^8.0.2",
"remark-rehype": "^7.0.0",
"remark-shortcodes": "^0.3.1",
"remark-slug": "^6.0.0",
"to-vfile": "^6.1.0",
"unified": "^9.0.0",
"unist-builder": "^2.0.3",
"unist-util-filter": "^2.0.2",
"unist-util-find-after": "^3.0.0",
"unist-util-inspect": "^6.0.0",
"unist-util-remove": "^2.0.0",
"unist-util-select": "^3.0.1",
"unist-util-visit": "^2.0.2"
"unist-util-visit": "^2.0.2",
"unist-util-visit-parents": "^3.0.2"
},
"devDependencies": {
"@release-it/conventional-changelog": "^1.1.4",
Expand Down
17 changes: 4 additions & 13 deletions src/plugins/ruby.ts
@@ -1,7 +1,6 @@
import {Handler} from 'mdast-util-to-hast';
import { Handler } from 'mdast-util-to-hast';
import all from 'mdast-util-to-hast/lib/all';
import {Plugin} from 'unified';
import {Parent} from 'unist';
import { Plugin } from 'unified';
import u from 'unist-builder';

// remark
Expand All @@ -24,7 +23,7 @@ const tokenizer: Tokenizer = function (eat, value, silent) {
return eat(eaten)({
type: 'ruby',
children: this.tokenizeInline(inlineContent, now),
data: {hName: 'ruby', rubyText},
data: { hName: 'ruby', rubyText },
});
};

Expand All @@ -34,24 +33,16 @@ tokenizer.locator = locateRuby;
export const attacher: Plugin = function () {
if (!this.Parser) return;

const {inlineTokenizers, inlineMethods} = this.Parser.prototype;
const { inlineTokenizers, inlineMethods } = this.Parser.prototype;
inlineTokenizers.ruby = tokenizer;
inlineMethods.splice(inlineMethods.indexOf('text'), 0, 'ruby');
};

// rehype
export const handler: Handler = (h, node) => {
const rtStart =
(node as Parent).children.length > 0
? (node as Parent).children[(node as Parent).children.length - 1]
.position!.end
: node.position!.start;

const rtNode = h(
{
type: 'element',
start: rtStart,
end: node.position!.end,
},
'rt',
[u('text', node.data!.rubyText as string)],
Expand Down
93 changes: 93 additions & 0 deletions src/plugins/section.ts
@@ -0,0 +1,93 @@
// derived from remark-sectionize
// https://github.com/jake-low/remark-sectionize
// MIT License
// original: 2019 Jake Low
// modified: 2020 Yasuaki Uechi

import { Parent } from 'mdast';
import findAfter from 'unist-util-find-after';
import visit from 'unist-util-visit-parents';
import { roleMappingTable, roles } from '../utils/wai-aria';

const MAX_HEADING_DEPTH = 6;

export function plugin() {
return (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,
);
}
};
}

function sectionize(node: any, ancestors: Parent[]) {
const start = node;
const depth = start.depth;
const parent = ancestors[ancestors.length - 1];

const isEnd = (node: any) =>
(node.type === 'heading' && node.depth <= depth) || node.type === 'export';
const end = findAfter(parent, start, isEnd);

const startIndex = parent.children.indexOf(start);
const endIndex = parent.children.indexOf(end);

const between = parent.children.slice(
startIndex,
endIndex > 0 ? endIndex : undefined,
);

let type = 'section';

const hProperties = node.data?.hProperties;
if (hProperties) {
node.data.hProperties = {};

const props = Object.keys(hProperties);

// {hidden} specifier
if (props.includes('hidden')) {
node.data.hProperties.style = 'display: none;';
}

// {@toc} specifier
const ariaProp = props
.filter((prop) => prop.startsWith('@'))
.map((id) => id.slice(1))
.find((key) => roles.includes(key));
if (ariaProp) {
const role = `doc-${ariaProp}`;
type = roleMappingTable[role][0];
hProperties.role = role;
delete hProperties['@' + ariaProp];
}
}

const isDuplicated = parent.type === 'section';
if (isDuplicated) {
if (parent.data?.hProperties) {
parent.data.hProperties = {
...(parent.data.hProperties as any),
...hProperties,
};
}
return;
}

const section = {
type,
data: {
hName: type,
hProperties,
},
depth: depth,
children: between,
} as any;

parent.children.splice(startIndex, section.children.length, section);
}
26 changes: 25 additions & 1 deletion src/revive-parse.ts
@@ -1,24 +1,48 @@
import attr from 'remark-attr';
import breaks from 'remark-breaks';
import footnotes from 'remark-footnotes';
import frontmatter from 'remark-frontmatter';
import math from 'remark-math';
import markdown from 'remark-parse';
import slug from 'remark-slug';
import unified from 'unified';
import { attacher as code } from './plugins/code';
import { attacher as fencedBlock } from './plugins/fenced-block';
import { plugin as metadata } from './plugins/metadata';
import { attacher as ruby } from './plugins/ruby';
import { plugin as section } from './plugins/section';
import { plugin as toc } from './plugins/toc';
import { inspect } from './utils/debug';

export default [
[markdown, { gfm: true, commonmark: true }],
fencedBlock,
ruby,
breaks,
[footnotes, { inlineNotes: true }],
code,
ruby,
math,
[
attr,
{
enableAtxHeaderInline: true,
scope: 'permissive',
elements: [
'link',
'atxHeading',
'strong',
'emphasis',
'deletion',
'code',
'fencedCode',
'reference',
'footnoteCall',
'autoLink',
],
},
],
slug,
section,
toc,
frontmatter,
metadata,
Expand Down
2 changes: 0 additions & 2 deletions src/revive-rehype.ts
@@ -1,5 +1,4 @@
import raw from 'rehype-raw';
import slug from 'rehype-slug';
import remark2rehype from 'remark-rehype';
import unified from 'unified';
import { handler as code } from './plugins/code';
Expand All @@ -22,6 +21,5 @@ export default [
raw,
figure,
math,
slug,
inspect('hast'),
] as unified.PluggableList<unified.Settings>;
4 changes: 4 additions & 0 deletions src/utils/wai-aria.ts
Expand Up @@ -44,3 +44,7 @@ export const roleMappingTable: RoleMappingTable = {
'doc-tip': ['aside'],
'doc-toc': ['nav', 'section'],
};

export const roles = Object.keys(roleMappingTable).map((key) =>
key.replace(/^doc-/, ''),
);
49 changes: 31 additions & 18 deletions tests/index.test.ts
Expand Up @@ -5,7 +5,7 @@ function partial(body: string) {
return lib.stringify(body, { partial: true });
}

it.skip('handle custom attributes', () => {
it('handle custom attributes', () => {
// MEMO:
// https://github.com/sethvincent/remark-bracketed-spans
// https://github.com/Paperist/remark-crossref/
Expand All @@ -14,13 +14,11 @@ it.skip('handle custom attributes', () => {
partial(`
# Introduction {#introduction}
`),
).toBe(`<h1 id="introduction">Introduction</h1>`);
).toBe(`<section id="introduction"><h1>Introduction</h1></section>`);

expect(
partial(`
[text in the span]{.class .other-class key=val another=example}
`),
).toBe(`<h1 id="introduction">Introduction</h1>`);
expect(partial(`# Hello {hidden}`)).toBe(
`<section id="hello"><h1 style="display: none;">Hello</h1></section>`,
);
});

it('handle role', () => {
Expand All @@ -30,7 +28,9 @@ it('handle role', () => {
# Tips
:::
`),
).toBe(`<aside role="doc-tip"><h1 id="tips">Tips</h1></aside>`);
).toBe(
`<aside role="doc-tip"><section id="tips"><h1>Tips</h1></section></aside>`,
);

expect(
partial(`
Expand All @@ -39,8 +39,19 @@ it('handle role', () => {
:::
`),
).toBe(
`<section role="doc-appendix"><h1 id="appendix">Appendix</h1></section>`,
`<section role="doc-appendix" id="appendix"><h1>Appendix</h1></section>`,
);

expect(
partial(`
# Table of Contents {@toc}
- [Intro](intro.md)
`),
)
.toBe(`<nav id="table-of-contents" role="doc-toc"><h1>Table of Contents</h1><ul>
<li><a href="intro.md">Intro</a></li>
</ul></nav>`);
});

it('reject incorrect fences', () => {
Expand All @@ -54,9 +65,9 @@ it('reject incorrect fences', () => {
`),
).toBe(
`<p>::::appendix<br>
:::::nested</p>
<h1 id="title">Title</h1>
<p>:::::<br>
:::::nested<br>
# Title<br>
:::::<br>
::::</p>`,
);

Expand All @@ -69,7 +80,9 @@ it('reject incorrect fences', () => {
:::
`),
).toBe(
`<div class="appendix"><p>:::::nested</p><h1 id="title">Title</h1><p>:::::</p></div>`,
`<div class="appendix"><p>:::::nested<br>
# Title<br>
:::::</p></div>`,
);
});

Expand All @@ -82,7 +95,7 @@ test
:::
`),
).toBe(
`<div class="appendix"><h1 id="appendix">Appendix</h1><p>test</p></div>`,
`<div class="appendix"><section id="appendix"><h1>Appendix</h1><p>test</p></section></div>`,
);

expect(
Expand All @@ -98,7 +111,7 @@ Another block
:::
`),
).toBe(
`<div><h1 id="plain-block">Plain block</h1></div>
`<div><section id="plain-block"><h1>Plain block</h1></section></div>
<hr>
<div class="another"><p>Another block</p></div>`,
);
Expand All @@ -114,7 +127,7 @@ A
:::
`),
).toBe(
`<div class="appendix"><p>A</p><div class="nested"><h1 id="title">Title</h1></div></div>`,
`<div class="appendix"><p>A</p><div class="nested"><section id="title"><h1>Title</h1></section></div></div>`,
);
});

Expand All @@ -133,7 +146,7 @@ it('stringify math', () => {
);
});

it('stringify ruby', () => {
it('ruby', () => {
expect(partial('{A|B}')).toBe(`<p><ruby>A<rt>B</rt></ruby></p>`);
});

Expand All @@ -153,7 +166,7 @@ it('stringify markdown string into html document', () => {
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1 id="こんにちは">こんにちは</h1>
<section id="こんにちは"><h1>こんにちは</h1></section>
</body>
</html>
`);
Expand Down

0 comments on commit dd74d39

Please sign in to comment.