Skip to content

Commit 89ab0c4

Browse files
committed
chore(TokenTree): Improved token tree building based off of open/closed rather than level
1 parent fbd9190 commit 89ab0c4

22 files changed

Lines changed: 2339 additions & 1762 deletions

‎src/Renderer.js‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React from 'react';
2+
import assign from './assign';
3+
import defaultComponents from './defaultComponents';
4+
import defaultRemarkableProps from './defaultRemarkableProps';
5+
import defaultTokens from './defaultTokens';
6+
import TokenTree from './TokenTree';
7+
8+
export default class Renderer {
9+
constructor(options = {}) {
10+
this.options = assign({
11+
keyGen: (token, index) => index,
12+
}, options, {
13+
components: assign({}, defaultComponents, options.components),
14+
remarkableProps: assign({}, defaultRemarkableProps, options.remarkableProps),
15+
tokens: assign({}, defaultTokens, options.tokens),
16+
});
17+
}
18+
19+
render(tokens = [], remarkableOptions) {
20+
return this.renderTokenTree(
21+
this.buildTokenTree(tokens, remarkableOptions)
22+
);
23+
}
24+
25+
buildTokenTree(tokens, remarkableOptions) {
26+
return new TokenTree(tokens, this.options, remarkableOptions);
27+
}
28+
29+
renderTokenTree(token, index = 0) {
30+
if (Array.isArray(token)) return token.map((token, indx) => this.renderTokenTree(token, indx));
31+
if (!token || !token.type) return token;
32+
if (!this.options.components[token.type]) return null;
33+
34+
return React.createElement(
35+
this.options.components[token.type], assign({}, token.props, {
36+
key: this.options.keyGen(token, index),
37+
}), this.renderTokenTree(token.children),
38+
);
39+
}
40+
}

‎src/TokenTree.js‎

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
const OPEN_IDENTIFIER = '_open';
2+
const CLOSE_IDENTIFIER = '_close';
3+
const INLINE_TYPE = 'inline';
4+
5+
const isOpenToken = ({ type }) => type.includes(OPEN_IDENTIFIER);
6+
const isCloseToken = ({ type }) => type.includes(CLOSE_IDENTIFIER);
7+
const isInlineToken = ({ type }) => type === INLINE_TYPE;
8+
9+
export default class TokenTree {
10+
constructor(tokens = [], options, rOptions) {
11+
this.i = -1;
12+
this.tokens = tokens;
13+
this.options = options;
14+
this.rOptions = rOptions;
15+
16+
return this.buildTokenTree();
17+
}
18+
19+
getType(token) {
20+
return typeof this.options.tokens[token.type] === 'function'
21+
? this.options.tokens[token.type](token, this.rOptions)
22+
: this.options.tokens[token.type];
23+
}
24+
25+
buildToken(token, type = this.getType(token), children) {
26+
if (Array.isArray(type)) {
27+
return type.reduceRight((child, typee, index) => (
28+
this.buildToken(token, typee, index < type.length - 1 ? [child] : undefined)
29+
), null);
30+
}
31+
32+
if (!type) {
33+
return token.content;
34+
}
35+
36+
return ({
37+
type,
38+
props: this.buildTokenProps(type, token),
39+
children: token.content,
40+
});
41+
}
42+
43+
buildTokenTree() {
44+
const ts = this.tokens;
45+
const collection = [];
46+
47+
while (++this.i < ts.length) {
48+
if (isOpenToken(ts[this.i])) {
49+
collection.push(
50+
this.buildToken(
51+
ts[this.i],
52+
this.getType(ts[this.i]),
53+
this.buildTokenTree()
54+
)
55+
);
56+
} else if (isCloseToken(ts[this.i])) {
57+
return collection;
58+
} else if (isInlineToken(ts[this.i])) {
59+
new TokenTree(ts[this.i].children, this.options, this.rOptions)
60+
.forEach((token) => collection.push(token));
61+
} else {
62+
collection.push(this.buildToken(ts[this.i]));
63+
}
64+
}
65+
66+
return collection;
67+
}
68+
69+
buildTokenProps(type, token) {
70+
const props = {};
71+
72+
if (typeof this.options.components[type] === 'function') {
73+
props.options = this.rOptions;
74+
}
75+
76+
[token, this.rOptions].forEach((prps) => {
77+
Object.keys(prps).forEach((prop) => {
78+
const propValue = this.resolveProp(type, prop, prps[prop]);
79+
if (propValue) props[propValue.key] = propValue.value;
80+
});
81+
});
82+
83+
return props;
84+
}
85+
86+
resolveProp(type, prop, propValue) {
87+
const component = this.options.components[type];
88+
const resolver = this.options.remarkableProps[prop];
89+
90+
if (!resolver) return;
91+
92+
if (typeof resolver === 'function') {
93+
return this.resolveFunctionProp(type, resolver(propValue, type), prop, propValue);
94+
}
95+
96+
if (typeof resolver === 'string') {
97+
return this.resolveStringProp(resolver, propValue);
98+
}
99+
100+
if (resolver === true || typeof component === 'function') {
101+
return this.resolveTrueProp(prop, propValue);
102+
}
103+
}
104+
105+
resolveFunctionProp(type, keyValue, prop, propValue) {
106+
if (keyValue) {
107+
return {
108+
key: keyValue.key || prop,
109+
value: keyValue.hasOwnProperty('value')
110+
? keyValue.value
111+
: propValue,
112+
};
113+
}
114+
}
115+
116+
resolveStringProp(resolver, propValue) {
117+
return {
118+
key: resolver,
119+
value: propValue,
120+
};
121+
}
122+
123+
resolveTrueProp(prop, propValue) {
124+
return {
125+
key: prop,
126+
value: propValue,
127+
};
128+
}
129+
}

src/utils.js renamed to src/assign.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
export const assign = Object.assign || ((target, ...args) => {
1+
export default Object.assign || ((target, ...args) => {
22
for (let i = 1; i < args.length; i++) {
33
const source = args[i];
44

5-
for (let key in source) {
5+
for (const key in source) {
66
if (Object.prototype.hasOwnProperty.call(source, key)) {
77
target[key] = source[key];
88
}

‎src/defaultComponents.js‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export default {
2+
a: 'a',
3+
blockquote: 'blockquote',
4+
br: 'br',
5+
code: 'code',
6+
del: 'del',
7+
em: 'em',
8+
h1: 'h1',
9+
h2: 'h2',
10+
h3: 'h3',
11+
h4: 'h4',
12+
h5: 'h5',
13+
h6: 'h6',
14+
hr: 'hr',
15+
html: 'div',
16+
img: 'img',
17+
ins: 'ins',
18+
li: 'li',
19+
mark: 'mark',
20+
ol: 'ol',
21+
p: 'p',
22+
pre: 'pre',
23+
strong: 'strong',
24+
sub: 'sub',
25+
sup: 'sup',
26+
table: 'table',
27+
tbody: 'tbody',
28+
td: 'td',
29+
th: 'th',
30+
thead: 'thead',
31+
tr: 'tr',
32+
ul: 'ul',
33+
};

‎src/defaultRemarkableProps.js‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export default {
2+
align: (align) => align ? ({
3+
key: 'style',
4+
value: { textAlign: align },
5+
}) : false,
6+
alt: true,
7+
block: false,
8+
content: (content, type) => type === 'html' && ({
9+
key: 'dangerouslySetInnerHTML',
10+
value: { __html: content },
11+
}),
12+
hLevel: false,
13+
href: true,
14+
level: false,
15+
lines: false,
16+
linkTarget: (target, type) => type === 'a' && ({
17+
key: 'target',
18+
}),
19+
order: false,
20+
params: false,
21+
src: true,
22+
tight: false,
23+
title: true,
24+
type: false,
25+
};

‎src/defaultTokens.js‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export default {
2+
blockquote_open: 'blockquote',
3+
bullet_list_open: 'ul',
4+
code: 'code',
5+
del_open: 'del',
6+
em_open: 'em',
7+
fence: ['pre', 'code'],
8+
hardbreak: 'br',
9+
heading_open: ({ hLevel }) => `h${hLevel}`,
10+
hr: 'hr',
11+
htmlblock: (_, { html }) => html && 'html',
12+
image: 'img',
13+
ins_open: 'ins',
14+
link_open: 'a',
15+
list_item_open: 'li',
16+
mark_open: 'mark',
17+
ordered_list_open: 'ol',
18+
paragraph_open: 'p',
19+
softbreak: (_, { breaks }) => breaks ? 'br' : undefined,
20+
strong_open: 'strong',
21+
sub: 'sub',
22+
sup: 'sup',
23+
table_open: 'table',
24+
tbody_open: 'tbody',
25+
td_open: 'td',
26+
th_open: 'th',
27+
thead_open: 'thead',
28+
tr_open: 'tr',
29+
};

‎src/index.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as default } from './Renderer';

0 commit comments

Comments
 (0)