Skip to content

Commit e70bc02

Browse files
committed
Rewrite method-doc to generate HTML directly
1 parent 73758a8 commit e70bc02

File tree

7 files changed

+133
-80
lines changed

7 files changed

+133
-80
lines changed

.changeset/shaggy-news-fry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@codewars/marked-extensions': minor
3+
---
4+
5+
Rewrite `method-doc` to generate HTML directly

src/doc-tokens/doc-globals.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ export function replaceDocGlobals(language, pre, content) {
1414
});
1515
}
1616

17+
export const docGlobal = (value, language) => {
18+
switch (language) {
19+
// languages which should keep the global class
20+
case 'java':
21+
case 'csharp':
22+
case 'scala':
23+
return `<dfn class="doc-class">${value}</dfn>`;
24+
25+
// all other languages remove the global class
26+
default:
27+
return '';
28+
}
29+
};
30+
1731
function wrap(value, pre) {
1832
return pre ? value : `<dfn class="doc-class">${value}</dfn>`;
1933
}

src/doc-tokens/doc-names.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,34 @@ export function replaceDocNames(language, pre, content) {
7171
);
7272
}
7373

74+
export const docName = (value, language, type = 'Name') => {
75+
const v = (() => {
76+
switch (findStyle(type, language)) {
77+
case 'upper':
78+
return value.toUpperCase();
79+
80+
case 'kabob':
81+
return value.replace(/_/g, '-');
82+
83+
case 'camel':
84+
return camelCase(value);
85+
86+
case 'dollarCamel':
87+
return '$' + camelCase(value);
88+
89+
case 'upperCamel':
90+
return camelCase(value, true);
91+
92+
default:
93+
return value;
94+
}
95+
})();
96+
return `<dfn class="doc-name doc-name--${type.toLowerCase()}">${v}</dfn>`;
97+
};
98+
99+
export const docClass = (value, language) => docName(value, language, 'Class');
100+
export const docMethod = (value, language) => docName(value, language, 'Method');
101+
74102
function findStyle(type, language) {
75103
let style = Object.keys(STYLES[type] || []).find((style) => {
76104
let _style = STYLES[type][style];

src/doc-tokens/doc-types.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,17 @@ export function replaceDocTypes(language, pre, content) {
130130
});
131131
}
132132

133+
export function docType(value, language) {
134+
const nullable = !!value.match(/\?$/);
135+
value = value.replace('?', '').trim();
136+
value = unescapeHtml(value);
137+
value = maybeMapGeneric(language, value);
138+
if (nullable) {
139+
value = mapNullable(language, value);
140+
}
141+
return `<dfn class="doc-type">${escapeHtml(value)}</dfn>`;
142+
}
143+
133144
function mapNullable(language, value) {
134145
if (NULLABLE[language]) {
135146
const config = NULLABLE[language][value] || NULLABLE[language].default;

src/method-doc.js

Lines changed: 72 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,151 +1,145 @@
11
import { escapeHtml } from './strings';
2+
import { docGlobal } from './doc-tokens/doc-globals';
3+
import { docName, docClass, docMethod } from './doc-tokens/doc-names';
4+
import { docType } from './doc-tokens/doc-types';
25

3-
export function methodDoc(code, language) {
6+
export function methodDoc(code, language, render) {
47
try {
5-
let json = JSON.parse(code);
8+
const json = JSON.parse(code);
69

710
// support language specific overrides
811
if (json.languages && json.languages[language]) {
912
Object.assign(json, json.languages[language]);
1013
}
1114

12-
const md = [];
15+
const html = [];
1316

1417
if (!json.examplesOnly) {
1518
if (json.method) {
16-
md.push(methodHeader(json));
19+
html.push(methodHeader(json, language));
1720
}
1821
if (json.desc) {
19-
md.push(json.desc);
22+
html.push(render(json.desc));
2023
}
2124

22-
md.push('```%doc');
23-
25+
html.push(`<div class="block block--doc">`);
26+
html.push(`<dl>`);
2427
if (json.args) {
25-
md.push('Parameters:');
26-
md.push(parameters(json));
28+
html.push(`<dt>Parameters</dt>`);
29+
html.push(parameters(json, language).map(markdownDefD(render)).join('\n'));
2730
}
28-
md.push('Return Value:');
29-
md.push(returnType(json));
31+
32+
html.push(`<dt>Return Value</dt>`);
33+
html.push(`<dd>`);
34+
html.push(render(returnType(json, language)));
35+
html.push(`</dd>`);
36+
3037
if (json.constraints && json.constraints.length) {
31-
md.push('Constraints:');
32-
md.push(json.constraints.join('\n'));
38+
html.push(`<dt>Constraints</dt>`);
39+
html.push(json.constraints.map(markdownDefD(render)).join('\n'));
3340
}
3441
if (json.errors && json.errors.length) {
35-
md.push('Errors:');
36-
md.push(json.errors.join('\n'));
42+
html.push(`<dt>Errors</dt>`);
43+
html.push(json.errors.map(markdownDefD(render)).join('\n'));
3744
}
38-
md.push('```');
45+
46+
html.push(`</dl>`);
47+
html.push(`</div>`);
3948
}
4049

4150
if (json.examples && json.examples.length) {
42-
md.push('```%doc-block');
43-
md.push('#### Examples');
44-
md.push(exampleHeader(json));
45-
md.push(exampleRows(json));
46-
md.push('```');
51+
html.push(`<div class="block block--doc-block">`);
52+
html.push(`<h4>Examples</h4>`);
53+
html.push(`<table>`);
54+
html.push(`<thead>`);
55+
html.push(exampleHeader(json));
56+
html.push(`</thead>`);
57+
html.push(`<tbody>`);
58+
html.push(exampleRows(json));
59+
html.push(`</tbody>`);
60+
html.push(`</table>`);
61+
html.push(`</div>`);
4762
}
4863

49-
return md.join('\n');
64+
return html.join('\n');
5065
} catch (ex) {
51-
return '`Failed to render %jsonblock: ' + ex.message + '`';
66+
return `<code>Failed to render %method-doc: ${escapeHtml(ex.message)}</code>`;
5267
}
5368
}
5469

70+
const markdownDefD = (render) => (dd) => `<dd>${render(dd)}</dd>`;
71+
5572
function hasExampleNames(json) {
5673
return json.examples && json.examples.filter((e) => !!e.name).length > 0;
5774
}
75+
5876
function exampleRows(json) {
5977
const hasExamples = hasExampleNames(json);
60-
return json.examples.map((v) => exampleRow(json, v, hasExamples)).join('\n');
78+
return json.examples.map((v) => `<tr>${exampleRow(v, hasExamples)}</tr>`).join('\n');
6179
}
6280

63-
function exampleRow(json, example, hasExamples) {
64-
let md = '';
81+
function exampleRow(example, hasExamples) {
82+
const tds = [];
6583
if (hasExamples) {
66-
const name = example.name;
67-
md = `*${name || 'Example'}*|`;
68-
}
69-
70-
if (example.args) {
71-
md += example.args.map((arg) => formatExampleValue(arg)).join('|');
84+
tds.push(`<em>${example.name || 'Example'}</em>`);
7285
}
73-
md += `|${formatExampleValue(example.returns) || ''}`;
74-
return md;
86+
if (example.args) tds.push(...example.args.map(formatExampleValue));
87+
tds.push(formatExampleValue(example.returns) || '');
88+
return tds.map((t) => `<td>${t}</td>`).join('');
7589
}
7690

7791
function formatExampleValue(value) {
7892
return `<code>${JSON.stringify(value)}</code>`;
7993
}
8094

8195
function exampleHeader(json) {
82-
const line1 = [];
83-
const line2 = [];
84-
if (hasExampleNames(json)) {
85-
line1.push('|');
86-
line2.push('');
87-
}
88-
89-
Object.keys(getArgs(json)).forEach((key) => {
90-
line1.push(key);
91-
line2.push('');
92-
});
93-
line1.push('Return Value');
94-
return `${line1.join('|')}\n-${line2.join('|-')}|-`;
96+
const keys = hasExampleNames(json) ? [''] : [];
97+
keys.push(...Object.keys(getArgs(json)));
98+
keys.push('Return Value');
99+
return `<tr>${keys.map((k) => `<th>${k}</th>`).join('')}</tr>`;
95100
}
96101

97102
function getArgs(json) {
98103
return json.args || json.params || json.parameters || {};
99104
}
100105

101-
function methodName(json) {
102-
let globalName = json.global !== false ? `@@docGlobal:${json.global || 'Challenge'}.` : '';
106+
function methodName(json, language) {
103107
// if a class is provided, it will always be shown and overrides global
104-
if (json.class) {
105-
globalName = `@@docClass:${json.class}.`;
106-
}
107-
108-
return `${globalName}@@docMethod:${json.method}`;
108+
const prefix = json.class
109+
? docClass(json.class, language)
110+
: json.global !== false
111+
? docGlobal(json.global || 'Challenge', language)
112+
: '';
113+
return `${prefix}${prefix ? '.' : ''}${docMethod(json.method, language)}`;
109114
}
110115

111-
function methodHeader(json) {
112-
const args = Object.keys(getArgs(json)).map((key) => `\`@@docName:${key}\``);
113-
return `### \`${methodName(json)}\`(${args.join(', ')})`;
116+
function methodHeader(json, language) {
117+
const args = Object.keys(getArgs(json)).map((key) => `<code>${docName(key, language)}</code>`);
118+
return `<h3><code>${methodName(json, language)}</code>(${args.join(', ')})</h3>`;
114119
}
115120

116-
function parameters(json) {
121+
function parameters(json, language) {
117122
const args = getArgs(json);
118-
const params = Object.keys(args).map((key) => {
123+
return Object.keys(args).map((key) => {
119124
const arg = args[key];
120125
const type = typeof arg === 'string' ? arg : arg.type;
121-
let md = `@@docName:${key}: ${formatDocType(json, type, 'String')}`;
122-
if (arg.desc) {
123-
md += ` - ${arg.desc}`;
124-
}
125-
126-
return md;
126+
const md = `${docName(key, language)}: ${formatDocType(json, type, 'String', language)}`;
127+
return md + (arg.desc ? ` - ${arg.desc}` : '');
127128
});
128-
129-
return params.join('\n');
130129
}
131130

132-
function returnType(json) {
131+
function returnType(json, language) {
133132
if (json.returns) {
134133
const type = typeof json.returns === 'string' ? json.returns : json.returns.type;
135-
let md = formatDocType(json, type, 'void');
136-
if (json.returns.desc) {
137-
md += ` - ${json.returns.desc}`;
138-
}
139-
140-
return md;
134+
const md = formatDocType(json, type, 'void', language);
135+
return md + (json.returns.desc ? ` - ${json.returns.desc}` : '');
141136
}
142-
return '@@docType:void';
137+
return docType('void', language);
143138
}
144139

145-
function formatDocType(json, type, defaultValue) {
140+
function formatDocType(json, type, defaultValue, language) {
146141
if (json.formatTypes === false) {
147142
return `<dfn class="doc-type">${escapeHtml(type)}</dfn>`;
148143
}
149-
150-
return `@@docType:${type || defaultValue || 'null'}`;
144+
return docType(type || defaultValue || 'null', language);
151145
}

src/renderer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ function setupCode(options, result) {
4242
} else if (language === '%definitions' || language === '%doc') {
4343
return wrapInBlockDiv(language, renderDefinitions(result, code, render));
4444
} else if (language === '%method-doc') {
45-
return wrapInBlockDiv('docs method-doc', render(methodDoc(code, result.originalLanguage)));
45+
return wrapInBlockDiv('docs method-doc', methodDoc(code, result.originalLanguage, render));
4646
} else if (language === '%table-doc') {
4747
return wrapInBlockDiv('docs table-doc', tableDoc(code));
4848
} else if (language[0] === '%') {

test/method-doc.spec.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ describe('%method-doc', () => {
88
const example = process(marked, fixture('method-doc')).html();
99
expect(example).to.include('>Examples</h4>');
1010
expect(example).to.include('<th>files</th>');
11-
expect(example).to.include('<code><dfn class="doc-class">');
11+
// Used to have empty `<dfn class="doc-class"></dfn>` after global prefix was removed
12+
// expect(example).to.include('<code><dfn class="doc-class">');
1213
expect(example).to.include('- A filtered array of files that were opened</p>');
1314
expect(example).to.include('<td><code>[1,2,3]</code></td>');
1415
expect(example).to.include('<td><code>"js"</code></td>');

0 commit comments

Comments
 (0)