Skip to content

Commit cf0cdb8

Browse files
authored
Add support for semantic markup (#1)
* Revert "Remove semantic markup definitions/options." This reverts commit b5d3365. * Add missing marker. * Implement argument parsing with escaping. * Add basic 'is plugin type' test. * Implement first batch of semantic markup commands (the easy ones). * Implement O() and RV(). * Fix P() parsing. * Support serialization. * Add changelog fragment. * Simplify MarkDown formatting (remove CSS classes).
1 parent d8f4833 commit cf0cdb8

11 files changed

+893
-13
lines changed

.prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
/changelogs/changelog.yaml
77
/changelogs/fragments/*
88
/dist/
9+
/test-vectors.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
minor_changes:
2+
- "Add support for semantic markup (https://github.com/ansible-community/antsibull-docs-ts/pull/1)."

src/ansible.ts

+5
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,8 @@
77
export function isFQCN(input: string): boolean {
88
return input.match(/^[a-z0-9_]+\.[a-z0-9_]+(?:\.[a-z0-9_]+)+$/) !== null;
99
}
10+
11+
export function isPluginType(input: string): boolean {
12+
/* We do not want to hard-code a list of valid plugin types that might be inaccurate, so we simply check whether this is a valid kind of Python identifier usually used for plugin types. If ansible-core ever adds one with digits, we'll have to update this. */
13+
return /^[a-z_]+$/.test(input);
14+
}

src/html.ts

+67-1
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,54 @@
55
*/
66

77
import { HTMLOptions } from './opts';
8-
import { PartType, Paragraph } from './parser';
8+
import { OptionNamePart, PartType, Paragraph, ReturnValuePart } from './parser';
99

1010
export function quoteHTML(text: string): string {
1111
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1212
}
1313

14+
function formatOptionLike(
15+
part: OptionNamePart | ReturnValuePart,
16+
what: 'option' | 'retval',
17+
opts: HTMLOptions,
18+
): string {
19+
let url: string | undefined;
20+
if (part.plugin && opts.pluginOptionLikeLink) {
21+
url = opts.pluginOptionLikeLink(part.plugin, what, part.link, part.plugin === opts.current_plugin);
22+
}
23+
let link_start = '';
24+
let link_end = '';
25+
if (url) {
26+
link_start = `<a class="reference internal" href="${quoteHTML(
27+
encodeURI(url),
28+
)}"><span class="std std-ref"><span class="pre">`;
29+
link_end = '</span></span></a>';
30+
}
31+
let cls: string;
32+
let strong_start = '';
33+
let strong_end = '';
34+
if (what === 'option') {
35+
if (part.value === undefined) {
36+
cls = 'ansible-option';
37+
strong_start = '<strong>';
38+
strong_end = '</strong>';
39+
} else {
40+
cls = 'ansible-option-value';
41+
}
42+
} else {
43+
cls = 'ansible-return-value';
44+
}
45+
let text: string;
46+
if (part.value === undefined) {
47+
text = part.name;
48+
} else {
49+
text = `${part.name}=${part.value}`;
50+
}
51+
return `<code class="${cls} literal notranslate">${strong_start}${link_start}${quoteHTML(
52+
text,
53+
)}${link_end}${strong_end}</code>`;
54+
}
55+
1456
export function toHTML(paragraphs: Paragraph[], opts?: HTMLOptions): string {
1557
if (!opts) {
1658
opts = {};
@@ -59,6 +101,30 @@ export function toHTML(paragraphs: Paragraph[], opts?: HTMLOptions): string {
59101
case PartType.TEXT:
60102
result.push(quoteHTML(part.text));
61103
break;
104+
case PartType.ENV_VARIABLE:
105+
result.push(`<code class="xref std std-envvar literal notranslate">${quoteHTML(part.name)}</code>`);
106+
break;
107+
case PartType.OPTION_NAME:
108+
result.push(formatOptionLike(part, 'option', opts));
109+
break;
110+
case PartType.OPTION_VALUE:
111+
result.push(`<code class="ansible-value literal notranslate">${quoteHTML(part.value)}</code>`);
112+
break;
113+
case PartType.PLUGIN: {
114+
let url: string | undefined;
115+
if (opts.pluginLink) {
116+
url = opts.pluginLink(part.plugin);
117+
}
118+
if (url) {
119+
result.push(`<a href='${url}' class='module'>${quoteHTML(part.plugin.fqcn)}</a>`);
120+
} else {
121+
result.push(`<span class='module'>${quoteHTML(part.plugin.fqcn)}</span>`);
122+
}
123+
break;
124+
}
125+
case PartType.RETURN_VALUE:
126+
result.push(formatOptionLike(part, 'retval', opts));
127+
break;
62128
}
63129
}
64130
result.push('</p>');

src/md.ts

+53-1
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,40 @@
77
// CommonMark spec: https://spec.commonmark.org/current/
88

99
import { MDOptions } from './opts';
10-
import { PartType, Paragraph } from './parser';
10+
import { OptionNamePart, PartType, Paragraph, ReturnValuePart } from './parser';
1111

1212
export function quoteMD(text: string): string {
1313
return text.replace(/([!"#$%&'()*+,:;<=>?@[\\\]^_`{|}~-])/g, '\\$1');
1414
}
1515

16+
function formatOptionLike(part: OptionNamePart | ReturnValuePart, what: 'option' | 'retval', opts: MDOptions): string {
17+
let url: string | undefined;
18+
if (part.plugin && opts.pluginOptionLikeLink) {
19+
url = opts.pluginOptionLikeLink(part.plugin, what, part.link, part.plugin === opts.current_plugin);
20+
}
21+
let link_start = '';
22+
let link_end = '';
23+
if (url) {
24+
link_start = `<a href="${quoteMD(encodeURI(url))}">`;
25+
link_end = '</a>';
26+
}
27+
let strong_start = '';
28+
let strong_end = '';
29+
if (what === 'option') {
30+
if (part.value === undefined) {
31+
strong_start = '<strong>';
32+
strong_end = '</strong>';
33+
}
34+
}
35+
let text: string;
36+
if (part.value === undefined) {
37+
text = part.name;
38+
} else {
39+
text = `${part.name}=${part.value}`;
40+
}
41+
return `<code>${strong_start}${link_start}${quoteMD(text)}${link_end}${strong_end}</code>`;
42+
}
43+
1644
export function toMD(paragraphs: Paragraph[], opts?: MDOptions): string {
1745
if (!opts) {
1846
opts = {};
@@ -61,6 +89,30 @@ export function toMD(paragraphs: Paragraph[], opts?: MDOptions): string {
6189
case PartType.TEXT:
6290
line.push(quoteMD(part.text));
6391
break;
92+
case PartType.ENV_VARIABLE:
93+
line.push(`<code>${quoteMD(part.name)}</code>`);
94+
break;
95+
case PartType.OPTION_NAME:
96+
line.push(formatOptionLike(part, 'option', opts));
97+
break;
98+
case PartType.OPTION_VALUE:
99+
line.push(`<code>${quoteMD(part.value)}</code>`);
100+
break;
101+
case PartType.PLUGIN: {
102+
let url: string | undefined;
103+
if (opts.pluginLink) {
104+
url = opts.pluginLink(part.plugin);
105+
}
106+
if (url) {
107+
line.push(`[${quoteMD(part.plugin.fqcn)}](${quoteMD(encodeURI(url))})`);
108+
} else {
109+
line.push(`${quoteMD(part.plugin.fqcn)}`);
110+
}
111+
break;
112+
}
113+
case PartType.RETURN_VALUE:
114+
line.push(formatOptionLike(part, 'retval', opts));
115+
break;
64116
}
65117
}
66118
if (!line.length) {

src/opts.ts

+22-7
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,44 @@ export interface PluginIdentifier {
2121
type: string;
2222
}
2323

24-
/* eslint-disable-next-line @typescript-eslint/no-empty-interface */
2524
export interface ParsingOptions extends ErrorHandlingOptions {
2625
/** Should be provided if parsing documentation of a plugin/module/role. */
2726
current_plugin?: PluginIdentifier;
27+
28+
/** If set to 'true', only 'classic' Ansible docs markup is accepted. */
29+
only_classic_markup?: boolean;
2830
}
2931

30-
/* eslint-disable-next-line @typescript-eslint/no-empty-interface */
3132
interface CommonExportOptions extends ErrorHandlingOptions {
3233
/** Should be provided if rendering documentation for a plugin/module/role. */
3334
current_plugin?: PluginIdentifier;
3435
}
3536

36-
/* eslint-disable-next-line @typescript-eslint/no-empty-interface */
3737
export interface HTMLOptions extends CommonExportOptions {
3838
/** Provides a link to a plugin. */
3939
pluginLink?: (plugin: PluginIdentifier) => string | undefined;
40-
}
4140

42-
/* eslint-disable-next-line @typescript-eslint/no-empty-interface */
43-
export interface RSTOptions extends CommonExportOptions {}
41+
/** Provides a link to a plugin's option or return value. */
42+
pluginOptionLikeLink?: (
43+
plugin: PluginIdentifier,
44+
what: 'option' | 'retval',
45+
name: string[],
46+
current_plugin: boolean,
47+
) => string | undefined;
48+
}
4449

45-
/* eslint-disable-next-line @typescript-eslint/no-empty-interface */
4650
export interface MDOptions extends CommonExportOptions {
4751
/** Provides a link to a plugin. */
4852
pluginLink?: (plugin: PluginIdentifier) => string | undefined;
53+
54+
/** Provides a link to a plugin's option or return value. */
55+
pluginOptionLikeLink?: (
56+
plugin: PluginIdentifier,
57+
what: 'option' | 'retval',
58+
name: string[],
59+
current_plugin: boolean,
60+
) => string | undefined;
4961
}
62+
63+
/* eslint-disable-next-line @typescript-eslint/no-empty-interface */
64+
export interface RSTOptions extends CommonExportOptions {}

0 commit comments

Comments
 (0)