Skip to content

Commit

Permalink
Rewrite method-doc to generate HTML directly
Browse files Browse the repository at this point in the history
  • Loading branch information
kazk committed Nov 30, 2021
1 parent ba6db30 commit 2ed9114
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 80 deletions.
5 changes: 5 additions & 0 deletions .changeset/shaggy-news-fry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@codewars/marked-extensions': minor
---

Rewrite `method-doc` to generate HTML directly
14 changes: 14 additions & 0 deletions src/doc-tokens/doc-globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ export function replaceDocGlobals(language, pre, content) {
});
}

export const docGlobal = (value, language) => {
switch (language) {
// languages which should keep the global class
case 'java':
case 'csharp':
case 'scala':
return `<dfn class="doc-class">${value}</dfn>`;

// all other languages remove the global class
default:
return '';
}
};

function wrap(value, pre) {
return pre ? value : `<dfn class="doc-class">${value}</dfn>`;
}
28 changes: 28 additions & 0 deletions src/doc-tokens/doc-names.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,34 @@ export function replaceDocNames(language, pre, content) {
);
}

export const docName = (value, language, type = 'Name') => {
const v = (() => {
switch (findStyle(type, language)) {
case 'upper':
return value.toUpperCase();

case 'kabob':
return value.replace(/_/g, '-');

case 'camel':
return camelCase(value);

case 'dollarCamel':
return '$' + camelCase(value);

case 'upperCamel':
return camelCase(value, true);

default:
return value;
}
})();
return `<dfn class="doc-name doc-name--${type.toLowerCase()}">${v}</dfn>`;
};

export const docClass = (value, language) => docName(value, language, 'Class');
export const docMethod = (value, language) => docName(value, language, 'Method');

function findStyle(type, language) {
let style = Object.keys(STYLES[type] || []).find((style) => {
let _style = STYLES[type][style];
Expand Down
11 changes: 11 additions & 0 deletions src/doc-tokens/doc-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,17 @@ export function replaceDocTypes(language, pre, content) {
});
}

export function docType(value, language) {
const nullable = !!value.match(/\?$/);
value = value.replace('?', '').trim();
value = unescapeHtml(value);
value = maybeMapGeneric(language, value);
if (nullable) {
value = mapNullable(language, value);
}
return `<dfn class="doc-type">${escapeHtml(value)}</dfn>`;
}

function mapNullable(language, value) {
if (NULLABLE[language]) {
const config = NULLABLE[language][value] || NULLABLE[language].default;
Expand Down
150 changes: 72 additions & 78 deletions src/method-doc.js
Original file line number Diff line number Diff line change
@@ -1,151 +1,145 @@
import { escapeHtml } from './strings';
import { docGlobal } from './doc-tokens/doc-globals';
import { docName, docClass, docMethod } from './doc-tokens/doc-names';
import { docType } from './doc-tokens/doc-types';

export function methodDoc(code, language) {
export function methodDoc(code, language, render) {
try {
let json = JSON.parse(code);
const json = JSON.parse(code);

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

const md = [];
const html = [];

if (!json.examplesOnly) {
if (json.method) {
md.push(methodHeader(json));
html.push(methodHeader(json, language));
}
if (json.desc) {
md.push(json.desc);
html.push(render(json.desc));
}

md.push('```%doc');

html.push(`<div class="block block--doc">`);
html.push(`<dl>`);
if (json.args) {
md.push('Parameters:');
md.push(parameters(json));
html.push(`<dt>Parameters</dt>`);
html.push(parameters(json, language).map(markdownDefD(render)).join('\n'));
}
md.push('Return Value:');
md.push(returnType(json));

html.push(`<dt>Return Value</dt>`);
html.push(`<dd>`);
html.push(render(returnType(json, language)));
html.push(`</dd>`);

if (json.constraints && json.constraints.length) {
md.push('Constraints:');
md.push(json.constraints.join('\n'));
html.push(`<dt>Constraints</dt>`);
html.push(json.constraints.map(markdownDefD(render)).join('\n'));
}
if (json.errors && json.errors.length) {
md.push('Errors:');
md.push(json.errors.join('\n'));
html.push(`<dt>Errors</dt>`);
html.push(json.errors.map(markdownDefD(render)).join('\n'));
}
md.push('```');

html.push(`</dl>`);
html.push(`</div>`);
}

if (json.examples && json.examples.length) {
md.push('```%doc-block');
md.push('#### Examples');
md.push(exampleHeader(json));
md.push(exampleRows(json));
md.push('```');
html.push(`<div class="block block--doc-block">`);
html.push(`<h4>Examples</h4>`);
html.push(`<table>`);
html.push(`<thead>`);
html.push(exampleHeader(json));
html.push(`</thead>`);
html.push(`<tbody>`);
html.push(exampleRows(json));
html.push(`</tbody>`);
html.push(`</table>`);
html.push(`</div>`);
}

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

const markdownDefD = (render) => (dd) => `<dd>${render(dd)}</dd>`;

function hasExampleNames(json) {
return json.examples && json.examples.filter((e) => !!e.name).length > 0;
}

function exampleRows(json) {
const hasExamples = hasExampleNames(json);
return json.examples.map((v) => exampleRow(json, v, hasExamples)).join('\n');
return json.examples.map((v) => `<tr>${exampleRow(v, hasExamples)}</tr>`).join('\n');
}

function exampleRow(json, example, hasExamples) {
let md = '';
function exampleRow(example, hasExamples) {
const tds = [];
if (hasExamples) {
const name = example.name;
md = `*${name || 'Example'}*|`;
}

if (example.args) {
md += example.args.map((arg) => formatExampleValue(arg)).join('|');
tds.push(`<em>${example.name || 'Example'}</em>`);
}
md += `|${formatExampleValue(example.returns) || ''}`;
return md;
if (example.args) tds.push(...example.args.map(formatExampleValue));
tds.push(formatExampleValue(example.returns) || '');
return tds.map((t) => `<td>${t}</td>`).join('');
}

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

function exampleHeader(json) {
const line1 = [];
const line2 = [];
if (hasExampleNames(json)) {
line1.push('|');
line2.push('');
}

Object.keys(getArgs(json)).forEach((key) => {
line1.push(key);
line2.push('');
});
line1.push('Return Value');
return `${line1.join('|')}\n-${line2.join('|-')}|-`;
const keys = hasExampleNames(json) ? [''] : [];
keys.push(...Object.keys(getArgs(json)));
keys.push('Return Value');
return `<tr>${keys.map((k) => `<th>${k}</th>`).join('')}</tr>`;
}

function getArgs(json) {
return json.args || json.params || json.parameters || {};
}

function methodName(json) {
let globalName = json.global !== false ? `@@docGlobal:${json.global || 'Challenge'}.` : '';
function methodName(json, language) {
// if a class is provided, it will always be shown and overrides global
if (json.class) {
globalName = `@@docClass:${json.class}.`;
}

return `${globalName}@@docMethod:${json.method}`;
const prefix = json.class
? docClass(json.class, language)
: json.global !== false
? docGlobal(json.global || 'Challenge', language)
: '';
return `${prefix}${prefix ? '.' : ''}${docMethod(json.method, language)}`;
}

function methodHeader(json) {
const args = Object.keys(getArgs(json)).map((key) => `\`@@docName:${key}\``);
return `### \`${methodName(json)}\`(${args.join(', ')})`;
function methodHeader(json, language) {
const args = Object.keys(getArgs(json)).map((key) => `<code>${docName(key, language)}</code>`);
return `<h3><code>${methodName(json, language)}</code>(${args.join(', ')})</h3>`;
}

function parameters(json) {
function parameters(json, language) {
const args = getArgs(json);
const params = Object.keys(args).map((key) => {
return Object.keys(args).map((key) => {
const arg = args[key];
const type = typeof arg === 'string' ? arg : arg.type;
let md = `@@docName:${key}: ${formatDocType(json, type, 'String')}`;
if (arg.desc) {
md += ` - ${arg.desc}`;
}

return md;
const md = `${docName(key, language)}: ${formatDocType(json, type, 'String', language)}`;
return md + (arg.desc ? ` - ${arg.desc}` : '');
});

return params.join('\n');
}

function returnType(json) {
function returnType(json, language) {
if (json.returns) {
const type = typeof json.returns === 'string' ? json.returns : json.returns.type;
let md = formatDocType(json, type, 'void');
if (json.returns.desc) {
md += ` - ${json.returns.desc}`;
}

return md;
const md = formatDocType(json, type, 'void', language);
return md + (json.returns.desc ? ` - ${json.returns.desc}` : '');
}
return '@@docType:void';
return docType('void', language);
}

function formatDocType(json, type, defaultValue) {
function formatDocType(json, type, defaultValue, language) {
if (json.formatTypes === false) {
return `<dfn class="doc-type">${escapeHtml(type)}</dfn>`;
}

return `@@docType:${type || defaultValue || 'null'}`;
return docType(type || defaultValue || 'null', language);
}
2 changes: 1 addition & 1 deletion src/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function setupCode(options, result) {
} else if (language === '%definitions' || language === '%doc') {
return wrapInBlockDiv(language, renderDefinitions(result, code, render));
} else if (language === '%method-doc') {
return wrapInBlockDiv('docs method-doc', render(methodDoc(code, result.originalLanguage)));
return wrapInBlockDiv('docs method-doc', methodDoc(code, result.originalLanguage, render));
} else if (language === '%table-doc') {
return wrapInBlockDiv('docs table-doc', tableDoc(code));
} else if (language[0] === '%') {
Expand Down
3 changes: 2 additions & 1 deletion test/method-doc.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ describe('%method-doc', () => {
const example = process(marked, fixture('method-doc')).html();
expect(example).to.include('>Examples</h4>');
expect(example).to.include('<th>files</th>');
expect(example).to.include('<code><dfn class="doc-class">');
// Used to have empty `<dfn class="doc-class"></dfn>` after global prefix was removed
// expect(example).to.include('<code><dfn class="doc-class">');
expect(example).to.include('- A filtered array of files that were opened</p>');
expect(example).to.include('<td><code>[1,2,3]</code></td>');
expect(example).to.include('<td><code>"js"</code></td>');
Expand Down

0 comments on commit 2ed9114

Please sign in to comment.