antsibull-docs
Advanced tools
Comparing version 1.0.2 to 1.1.0
@@ -5,35 +5,61 @@ # antsibull\-docs \-\- TypeScript library for processing Ansible documentation markup Release Notes | ||
- <a href="#v1-0-2">v1\.0\.2</a> | ||
- <a href="#v1-1-0">v1\.1\.0</a> | ||
- <a href="#release-summary">Release Summary</a> | ||
- <a href="#minor-changes">Minor Changes</a> | ||
- <a href="#bugfixes">Bugfixes</a> | ||
- <a href="#v1-0-1">v1\.0\.1</a> | ||
- <a href="#v1-0-2">v1\.0\.2</a> | ||
- <a href="#release-summary-1">Release Summary</a> | ||
- <a href="#bugfixes-1">Bugfixes</a> | ||
- <a href="#v1-0-1">v1\.0\.1</a> | ||
- <a href="#release-summary-2">Release Summary</a> | ||
- <a href="#bugfixes-2">Bugfixes</a> | ||
- <a href="#v1-0-0">v1\.0\.0</a> | ||
- <a href="#release-summary-2">Release Summary</a> | ||
- <a href="#release-summary-3">Release Summary</a> | ||
- <a href="#v0-4-0">v0\.4\.0</a> | ||
- <a href="#release-summary-3">Release Summary</a> | ||
- <a href="#minor-changes">Minor Changes</a> | ||
- <a href="#release-summary-4">Release Summary</a> | ||
- <a href="#minor-changes-1">Minor Changes</a> | ||
- <a href="#breaking-changes--porting-guide">Breaking Changes / Porting Guide</a> | ||
- <a href="#bugfixes-2">Bugfixes</a> | ||
- <a href="#bugfixes-3">Bugfixes</a> | ||
- <a href="#v0-3-0">v0\.3\.0</a> | ||
- <a href="#release-summary-4">Release Summary</a> | ||
- <a href="#minor-changes-1">Minor Changes</a> | ||
- <a href="#v0-2-0">v0\.2\.0</a> | ||
- <a href="#release-summary-5">Release Summary</a> | ||
- <a href="#minor-changes-2">Minor Changes</a> | ||
- <a href="#v0-2-0">v0\.2\.0</a> | ||
- <a href="#release-summary-6">Release Summary</a> | ||
- <a href="#minor-changes-3">Minor Changes</a> | ||
- <a href="#breaking-changes--porting-guide-1">Breaking Changes / Porting Guide</a> | ||
- <a href="#bugfixes-3">Bugfixes</a> | ||
- <a href="#bugfixes-4">Bugfixes</a> | ||
- <a href="#v0-1-0">v0\.1\.0</a> | ||
- <a href="#release-summary-6">Release Summary</a> | ||
- <a href="#minor-changes-3">Minor Changes</a> | ||
- <a href="#release-summary-7">Release Summary</a> | ||
- <a href="#minor-changes-4">Minor Changes</a> | ||
- <a href="#breaking-changes--porting-guide-2">Breaking Changes / Porting Guide</a> | ||
- <a href="#bugfixes-4">Bugfixes</a> | ||
- <a href="#bugfixes-5">Bugfixes</a> | ||
- <a href="#v0-0-1">v0\.0\.1</a> | ||
- <a href="#release-summary-7">Release Summary</a> | ||
- <a href="#release-summary-8">Release Summary</a> | ||
<a id="v1-1-0"></a> | ||
## v1\.1\.0 | ||
<a id="release-summary"></a> | ||
### Release Summary | ||
Bugfix and feature release that improves markup parsing and generation with respect to whitespace handling and escaping\. | ||
<a id="minor-changes"></a> | ||
### Minor Changes | ||
* Allow to determine how to handle whitespace during parsing with the new <code>whitespace</code> option \([https\://github\.com/ansible\-community/antsibull\-docs\-ts/pull/295](https\://github\.com/ansible\-community/antsibull\-docs\-ts/pull/295)\)\. | ||
* Always remove some whitespace around <code>HORIZONTALLINE</code> \([https\://github\.com/ansible\-community/antsibull\-docs\-ts/pull/295](https\://github\.com/ansible\-community/antsibull\-docs\-ts/pull/295)\)\. | ||
* Apply postprocessing to RST and MarkDown to avoid generating invalid markup when input contains whitespace at potentially dangerous places \([https\://github\.com/ansible\-community/antsibull\-docs\-ts/pull/296](https\://github\.com/ansible\-community/antsibull\-docs\-ts/pull/296)\)\. | ||
<a id="bugfixes"></a> | ||
### Bugfixes | ||
* Do not apply URI encoding to visible URL \([https\://github\.com/ansible\-community/antsibull\-docs\-ts/pull/286](https\://github\.com/ansible\-community/antsibull\-docs\-ts/pull/286)\)\. | ||
* Fix RST escaping to handle other whitespace than spaces correctly \([https\://github\.com/ansible\-community/antsibull\-docs\-ts/pull/296](https\://github\.com/ansible\-community/antsibull\-docs\-ts/pull/296)\)\. | ||
* Improve handling of empty URL for links \([https\://github\.com/ansible\-community/antsibull\-docs\-ts/pull/286](https\://github\.com/ansible\-community/antsibull\-docs\-ts/pull/286)\)\. | ||
<a id="v1-0-2"></a> | ||
## v1\.0\.2 | ||
<a id="release-summary"></a> | ||
<a id="release-summary-1"></a> | ||
### Release Summary | ||
@@ -43,3 +69,3 @@ | ||
<a id="bugfixes"></a> | ||
<a id="bugfixes-1"></a> | ||
### Bugfixes | ||
@@ -52,3 +78,3 @@ | ||
<a id="release-summary-1"></a> | ||
<a id="release-summary-2"></a> | ||
### Release Summary | ||
@@ -58,3 +84,3 @@ | ||
<a id="bugfixes-1"></a> | ||
<a id="bugfixes-2"></a> | ||
### Bugfixes | ||
@@ -67,3 +93,3 @@ | ||
<a id="release-summary-2"></a> | ||
<a id="release-summary-3"></a> | ||
### Release Summary | ||
@@ -76,3 +102,3 @@ | ||
<a id="release-summary-3"></a> | ||
<a id="release-summary-4"></a> | ||
### Release Summary | ||
@@ -82,3 +108,3 @@ | ||
<a id="minor-changes"></a> | ||
<a id="minor-changes-1"></a> | ||
### Minor Changes | ||
@@ -93,3 +119,3 @@ | ||
<a id="bugfixes-2"></a> | ||
<a id="bugfixes-3"></a> | ||
### Bugfixes | ||
@@ -102,3 +128,3 @@ | ||
<a id="release-summary-4"></a> | ||
<a id="release-summary-5"></a> | ||
### Release Summary | ||
@@ -108,3 +134,3 @@ | ||
<a id="minor-changes-1"></a> | ||
<a id="minor-changes-2"></a> | ||
### Minor Changes | ||
@@ -117,3 +143,3 @@ | ||
<a id="release-summary-5"></a> | ||
<a id="release-summary-6"></a> | ||
### Release Summary | ||
@@ -123,3 +149,3 @@ | ||
<a id="minor-changes-2"></a> | ||
<a id="minor-changes-3"></a> | ||
### Minor Changes | ||
@@ -143,3 +169,3 @@ | ||
<a id="bugfixes-3"></a> | ||
<a id="bugfixes-4"></a> | ||
### Bugfixes | ||
@@ -152,3 +178,3 @@ | ||
<a id="release-summary-6"></a> | ||
<a id="release-summary-7"></a> | ||
### Release Summary | ||
@@ -158,3 +184,3 @@ | ||
<a id="minor-changes-3"></a> | ||
<a id="minor-changes-4"></a> | ||
### Minor Changes | ||
@@ -174,3 +200,3 @@ | ||
<a id="bugfixes-4"></a> | ||
<a id="bugfixes-5"></a> | ||
### Bugfixes | ||
@@ -184,5 +210,5 @@ | ||
<a id="release-summary-7"></a> | ||
<a id="release-summary-8"></a> | ||
### Release Summary | ||
Initial release\. |
@@ -46,3 +46,3 @@ "use strict"; | ||
formatRSTRef: (part) => `<span>${quoteHTML(part.text)}</span>`, | ||
formatURL: (part) => `<a href='${quoteHTMLArg(encodeURI(part.url))}'>${quoteHTML(encodeURI(part.url))}</a>`, | ||
formatURL: (part) => `<a href='${quoteHTMLArg(encodeURI(part.url))}'>${quoteHTML(part.url)}</a>`, | ||
formatText: (part) => quoteHTML(part.text), | ||
@@ -100,3 +100,3 @@ formatEnvVariable: (part) => `<code>${quoteHTML(part.name)}</code>`, | ||
formatRSTRef: (part) => `<span class='module'>${quoteHTML(part.text)}</span>`, | ||
formatURL: (part) => `<a href='${quoteHTMLArg(encodeURI(part.url))}'>${quoteHTML(encodeURI(part.url))}</a>`, | ||
formatURL: (part) => `<a href='${quoteHTMLArg(encodeURI(part.url))}'>${quoteHTML(part.url)}</a>`, | ||
formatText: (part) => quoteHTML(part.text), | ||
@@ -103,0 +103,0 @@ formatEnvVariable: (part) => `<code class="xref std std-envvar literal notranslate">${quoteHTML(part.name)}</code>`, |
@@ -9,2 +9,3 @@ "use strict"; | ||
exports.quoteMD = quoteMD; | ||
exports.postprocessMDParagraph = postprocessMDParagraph; | ||
exports.toMD = toMD; | ||
@@ -15,5 +16,12 @@ // CommonMark spec: https://spec.commonmark.org/current/ | ||
const format_1 = require("./format"); | ||
const util_1 = require("./util"); | ||
function quoteMD(text) { | ||
return text.replace(/([!"#$%&'()*+,:;<=>?@[\\\]^_`{|}~.-])/g, '\\$1'); | ||
} | ||
function postprocessMDParagraph(par) { | ||
return (0, util_1.splitLines)(par.trim()) | ||
.map((line) => line.trim().replace('\t', ' ')) | ||
.filter(Boolean) | ||
.join('\n'); | ||
} | ||
function formatOptionLike(part, url, what) { | ||
@@ -52,3 +60,3 @@ let link_start = ''; | ||
formatRSTRef: (part) => `${quoteMD(part.text)}`, | ||
formatURL: (part) => `[${quoteMD(encodeURI(part.url))}](${quoteMD(encodeURI(part.url))})`, | ||
formatURL: (part) => `[${quoteMD(part.url)}](${quoteMD(encodeURI(part.url))})`, | ||
formatText: (part) => quoteMD(part.text), | ||
@@ -67,6 +75,4 @@ formatEnvVariable: (part) => `<code>${quoteMD(part.name)}</code>`, | ||
(0, format_1.addToDestination)(line, paragraph, mergedOpts); | ||
if (!line.length) { | ||
line.push(' '); | ||
} | ||
result.push(line.join('')); | ||
const lineStr = postprocessMDParagraph(line.join('')); | ||
result.push(lineStr || ' '); | ||
} | ||
@@ -73,0 +79,0 @@ return result.join('\n\n'); |
@@ -33,3 +33,2 @@ "use strict"; | ||
const value = []; | ||
/* eslint-disable-next-line no-constant-condition */ | ||
while (true) { | ||
@@ -59,3 +58,2 @@ escapeOrComma.lastIndex = index; | ||
const value = []; | ||
/* eslint-disable-next-line no-constant-condition */ | ||
while (true) { | ||
@@ -62,0 +60,0 @@ escapeOrClosing.lastIndex = index; |
@@ -8,2 +8,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.processWhitespace = processWhitespace; | ||
exports.composeCommandMap = composeCommandMap; | ||
@@ -16,2 +17,5 @@ exports.composeCommandRE = composeCommandRE; | ||
const dom_1 = require("./dom"); | ||
function repr(text) { | ||
return JSON.stringify(text); | ||
} | ||
const IGNORE_MARKER = 'ignore:'; | ||
@@ -32,6 +36,6 @@ function parseOptionLike(text, opts, type, source) { | ||
if (!(0, ansible_1.isFQCN)(pluginFqcn)) { | ||
throw Error(`Plugin name "${pluginFqcn}" is not a FQCN`); | ||
throw Error(`Plugin name ${repr(pluginFqcn)} is not a FQCN`); | ||
} | ||
if (!(0, ansible_1.isPluginType)(pluginType)) { | ||
throw Error(`Plugin type "${pluginType}" is not valid`); | ||
throw Error(`Plugin type ${repr(pluginType)} is not valid`); | ||
} | ||
@@ -60,3 +64,3 @@ plugin = { fqcn: pluginFqcn, type: pluginType }; | ||
if (/[:#]/.test(text)) { | ||
throw Error(`Invalid option/return value name "${text}"`); | ||
throw Error(`Invalid option/return value name ${repr(text)}`); | ||
} | ||
@@ -73,2 +77,58 @@ return { | ||
} | ||
function addWhitespace(result, ws, whitespace, noNewlines) { | ||
if (whitespace === 'keep_single_newlines' && !noNewlines && ws.search(/[\n\r]/) >= 0) { | ||
result.push('\n'); | ||
} | ||
else { | ||
result.push(' '); | ||
} | ||
} | ||
function processWhitespace(text, whitespace, codeEnvironment, noNewlines) { | ||
if (whitespace === 'ignore') { | ||
return text; | ||
} | ||
const length = text.length; | ||
let index = 0; | ||
const result = []; | ||
const whitespaces = /([\s]+)/g; | ||
// The 'no-misleading-character-class' warning reported by eslint below can be safely | ||
// ignored since we have a list of distinct Unicode codepoints, and we don't rely on | ||
// them being part of the adjacent codepoints. | ||
/* eslint-disable-next-line no-misleading-character-class */ | ||
const spacesToKeep = /([\u00A0\u202F\u2007\u2060\u200B\u200C\u200D\uFEFF]+)/g; | ||
while (index < length) { | ||
whitespaces.lastIndex = index; | ||
const m = whitespaces.exec(text); | ||
if (!m) { | ||
result.push(text.slice(index)); | ||
break; | ||
} | ||
if (m.index > index) { | ||
result.push(text.slice(index, m.index)); | ||
} | ||
const ws = m[0]; | ||
const ws_length = ws.length; | ||
if (codeEnvironment) { | ||
result.push(ws.replace(/[\t\n\r]/g, ' ')); | ||
} | ||
else { | ||
let ws_index = 0; | ||
while (ws_index < ws_length) { | ||
spacesToKeep.lastIndex = ws_index; | ||
const wsm = spacesToKeep.exec(ws); | ||
if (!wsm) { | ||
addWhitespace(result, ws.slice(ws_index), whitespace, noNewlines); | ||
break; | ||
} | ||
if (wsm.index > ws_index) { | ||
addWhitespace(result, ws.slice(ws_index, wsm.index), whitespace, noNewlines); | ||
} | ||
result.push(wsm[0]); | ||
ws_index = wsm.index + wsm[0].length; | ||
} | ||
} | ||
index = m.index + ws_length; | ||
} | ||
return result.join(''); | ||
} | ||
const PARSER = [ | ||
@@ -80,4 +140,4 @@ // Classic Ansible docs markup: | ||
old_markup: true, | ||
process: (args, _, source) => { | ||
const text = args[0]; | ||
process: (args, _, source, whitespace) => { | ||
const text = processWhitespace(args[0], whitespace, false, true); | ||
return { type: dom_1.PartType.ITALIC, text: text, source: source }; | ||
@@ -90,4 +150,4 @@ }, | ||
old_markup: true, | ||
process: (args, _, source) => { | ||
const text = args[0]; | ||
process: (args, _, source, whitespace) => { | ||
const text = processWhitespace(args[0], whitespace, false, true); | ||
return { type: dom_1.PartType.BOLD, text: text, source: source }; | ||
@@ -100,6 +160,6 @@ }, | ||
old_markup: true, | ||
process: (args, _, source) => { | ||
const fqcn = args[0]; | ||
process: (args, _, source, whitespace) => { | ||
const fqcn = processWhitespace(args[0], whitespace, false, true); | ||
if (!(0, ansible_1.isFQCN)(fqcn)) { | ||
throw Error(`Module name "${fqcn}" is not a FQCN`); | ||
throw Error(`Module name ${repr(fqcn)} is not a FQCN`); | ||
} | ||
@@ -113,4 +173,4 @@ return { type: dom_1.PartType.MODULE, fqcn: fqcn, source: source }; | ||
old_markup: true, | ||
process: (args, _, source) => { | ||
const url = args[0]; | ||
process: (args, _, source, whitespace) => { | ||
const url = processWhitespace(args[0], whitespace, false, true); | ||
return { type: dom_1.PartType.URL, url: url, source: source }; | ||
@@ -123,5 +183,5 @@ }, | ||
old_markup: true, | ||
process: (args, _, source) => { | ||
const text = args[0]; | ||
const url = args[1]; | ||
process: (args, _, source, whitespace) => { | ||
const text = processWhitespace(args[0], whitespace, false, true); | ||
const url = processWhitespace(args[1], whitespace, false, true); | ||
return { type: dom_1.PartType.LINK, text: text, url: url, source: source }; | ||
@@ -134,5 +194,5 @@ }, | ||
old_markup: true, | ||
process: (args, _, source) => { | ||
const text = args[0]; | ||
const ref = args[1]; | ||
process: (args, _, source, whitespace) => { | ||
const text = processWhitespace(args[0], whitespace, false, true); | ||
const ref = processWhitespace(args[1], whitespace, false, true); | ||
return { type: dom_1.PartType.RST_REF, text: text, ref: ref, source: source }; | ||
@@ -145,4 +205,4 @@ }, | ||
old_markup: true, | ||
process: (args, _, source) => { | ||
const text = args[0]; | ||
process: (args, _, source, whitespace) => { | ||
const text = processWhitespace(args[0], whitespace, true, true); | ||
return { type: dom_1.PartType.CODE, text: text, source: source }; | ||
@@ -154,2 +214,3 @@ }, | ||
parameters: 0, | ||
stripSurroundingWhitespace: true, | ||
old_markup: true, | ||
@@ -165,14 +226,15 @@ process: (_, __, source) => { | ||
escapedArguments: true, | ||
process: (args, _, source) => { | ||
const m = /^([^#]*)#(.*)$/.exec(args[0]); | ||
process: (args, _, source, whitespace) => { | ||
const plugin = processWhitespace(args[0], whitespace, false, true); | ||
const m = /^([^#]*)#(.*)$/.exec(plugin); | ||
if (!m) { | ||
throw Error(`Parameter "${args[0]}" is not of the form FQCN#type`); | ||
throw Error(`Parameter ${repr(args[0])} is not of the form FQCN#type`); | ||
} | ||
const fqcn = m[1]; | ||
if (!(0, ansible_1.isFQCN)(fqcn)) { | ||
throw Error(`Plugin name "${fqcn}" is not a FQCN`); | ||
throw Error(`Plugin name ${repr(fqcn)} is not a FQCN`); | ||
} | ||
const type = m[2]; | ||
if (!(0, ansible_1.isPluginType)(type)) { | ||
throw Error(`Plugin type "${type}" is not valid`); | ||
throw Error(`Plugin type ${repr(type)} is not valid`); | ||
} | ||
@@ -186,4 +248,4 @@ return { type: dom_1.PartType.PLUGIN, plugin: { fqcn: fqcn, type: type }, source: source }; | ||
escapedArguments: true, | ||
process: (args, _, source) => { | ||
const env = args[0]; | ||
process: (args, _, source, whitespace) => { | ||
const env = processWhitespace(args[0], whitespace, true, true); | ||
return { type: dom_1.PartType.ENV_VARIABLE, name: env, source: source }; | ||
@@ -196,4 +258,4 @@ }, | ||
escapedArguments: true, | ||
process: (args, _, source) => { | ||
const value = args[0]; | ||
process: (args, _, source, whitespace) => { | ||
const value = processWhitespace(args[0], whitespace, true, true); | ||
return { type: dom_1.PartType.OPTION_VALUE, value: value, source: source }; | ||
@@ -206,4 +268,4 @@ }, | ||
escapedArguments: true, | ||
process: (args, opts, source) => { | ||
const value = args[0]; | ||
process: (args, opts, source, whitespace) => { | ||
const value = processWhitespace(args[0], whitespace, true, true); | ||
return parseOptionLike(value, opts, dom_1.PartType.OPTION_NAME, source); | ||
@@ -216,4 +278,4 @@ }, | ||
escapedArguments: true, | ||
process: (args, opts, source) => { | ||
const value = args[0]; | ||
process: (args, opts, source, whitespace) => { | ||
const value = processWhitespace(args[0], whitespace, true, true); | ||
return parseOptionLike(value, opts, dom_1.PartType.RETURN_VALUE, source); | ||
@@ -239,3 +301,5 @@ }, | ||
var _a; | ||
// TODO: handle opts.whitespace! | ||
const result = []; | ||
const whitespace = opts.whitespace || 'ignore'; | ||
const length = input.length; | ||
@@ -251,3 +315,3 @@ let index = 0; | ||
type: dom_1.PartType.TEXT, | ||
text: text, | ||
text: processWhitespace(text, whitespace, false, false), | ||
source: opts.addSource ? text : undefined, | ||
@@ -258,10 +322,3 @@ }); | ||
} | ||
if (match.index > index) { | ||
const text = input.slice(index, match.index); | ||
result.push({ | ||
type: dom_1.PartType.TEXT, | ||
text: text, | ||
source: opts.addSource ? text : undefined, | ||
}); | ||
} | ||
const prevIndex = index; | ||
index = match.index; | ||
@@ -275,4 +332,21 @@ let cmd = match[0]; | ||
if (!command) { | ||
throw Error(`Internal error: unknown command "${cmd}"`); | ||
throw Error(`Internal error: unknown command ${repr(cmd)}`); | ||
} | ||
if (match.index > prevIndex) { | ||
let text = input.slice(prevIndex, match.index); | ||
if (command.stripSurroundingWhitespace) { | ||
let end = text.length; | ||
while (end > 0 && /[ \t]/.test(text[end - 1])) { | ||
end -= 1; | ||
} | ||
if (end < text.length) { | ||
text = text.slice(0, end); | ||
} | ||
} | ||
result.push({ | ||
type: dom_1.PartType.TEXT, | ||
text: processWhitespace(text, whitespace, false, false), | ||
source: opts.addSource ? text : undefined, | ||
}); | ||
} | ||
let args; | ||
@@ -292,3 +366,3 @@ let error; | ||
try { | ||
result.push(command.process(args, opts, source)); | ||
result.push(command.process(args, opts, source, whitespace)); | ||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ | ||
@@ -305,3 +379,3 @@ } | ||
const errorSource = ((_a = opts.helpfulErrors) !== null && _a !== void 0 ? _a : true) | ||
? `"${input.slice(index, endIndex)}"` | ||
? repr(input.slice(index, endIndex)) | ||
: `${cmd}${command.parameters > 0 ? '()' : ''}`; | ||
@@ -324,2 +398,7 @@ error = `While parsing ${errorSource} at index ${match.index + 1}${where}: ${error}`; | ||
index = endIndex; | ||
if (command.stripSurroundingWhitespace) { | ||
while (index < length && /[ \t]/.test(input[index])) { | ||
index += 1; | ||
} | ||
} | ||
} | ||
@@ -326,0 +405,0 @@ return result; |
@@ -9,11 +9,13 @@ "use strict"; | ||
exports.quoteRST = quoteRST; | ||
exports.postprocessRSTParagraph = postprocessRSTParagraph; | ||
exports.toRST = toRST; | ||
const opts_1 = require("./opts"); | ||
const format_1 = require("./format"); | ||
const util_1 = require("./util"); | ||
function quoteRST(text, escape_starting_whitespace = false, escape_ending_whitespace = false, must_not_be_empty = false) { | ||
text = text.replace(/([\\<>_*`])/g, '\\$1'); | ||
if (escape_ending_whitespace && text.endsWith(' ')) { | ||
if (escape_ending_whitespace && /\s$/.test(text.substring(text.length - 1))) { | ||
text = text + '\\ '; | ||
} | ||
if (escape_starting_whitespace && text.startsWith(' ')) { | ||
if (escape_starting_whitespace && /^\s/.test(text)) { | ||
text = '\\ ' + text; | ||
@@ -26,2 +28,91 @@ } | ||
} | ||
function removeBackslashSpace(line) { | ||
let start = 0; | ||
let end = line.length; | ||
while (true) { | ||
// Remove starting '\ '. These have no effect. | ||
while ((0, util_1.startsWith)(line, '\\ ', start, end)) { | ||
start += 2; | ||
} | ||
// If the line now starts with regular whitespace, trim it. | ||
if ((0, util_1.startsWith)(line, ' ', start, end)) { | ||
start += 1; | ||
} | ||
else { | ||
// If there is none, we're done. | ||
break; | ||
} | ||
// Remove more starting whitespace, and then check again for leading '\ ' etc. | ||
while ((0, util_1.startsWith)(line, ' ', start, end)) { | ||
start += 1; | ||
} | ||
} | ||
while (true) { | ||
/* | ||
Remove trailing '\ ' resp. '\' (after line.trim()). These actually have an effect, | ||
since they remove the linebreak. *But* if our markup generator emits '\ ' followed | ||
by a line break, we still want the line break to count, so this is actually fixing | ||
a bug. | ||
*/ | ||
if ((0, util_1.endsWith)(line, '\\', start, end)) { | ||
end -= 1; | ||
} | ||
while ((0, util_1.endsWith)(line, '\\ ', start, end)) { | ||
end -= 2; | ||
} | ||
// If the line now ends with regular whitespace, trim it. | ||
if ((0, util_1.endsWith)(line, ' ', start, end)) { | ||
end -= 1; | ||
} | ||
else { | ||
// If there is none, we're done. | ||
break; | ||
} | ||
// Remove more ending whitespace, and then check again for trailing '\' etc. | ||
while ((0, util_1.endsWith)(line, ' ', start, end)) { | ||
end -= 1; | ||
} | ||
} | ||
// Return subset of the line | ||
line = line.substring(start, end); | ||
line = line.replace(/\\ (?:\\ )+/g, '\\ '); | ||
line = line.replace(/(?<![\\])([ ])\\ (?![`])/g, '$1'); | ||
line = line.replace(/(?<!:`)\\ ([ .])/g, '$1'); | ||
return line; | ||
} | ||
function checkLine(index, lines, line) { | ||
if (index < 0 || index >= lines.length) { | ||
return false; | ||
} | ||
return lines[index] === line; | ||
} | ||
function modifyLine(index, line, lines) { | ||
const raw_html = '.. raw:: html'; | ||
const dashes = '------------'; | ||
const hr = ' <hr>'; | ||
if (line !== '' && line !== raw_html && line !== dashes && line !== hr) { | ||
return true; | ||
} | ||
if (line === raw_html || line === dashes) { | ||
return false; | ||
} | ||
if (line === hr && checkLine(index - 2, lines, raw_html)) { | ||
return false; | ||
} | ||
if (line === '' && | ||
(checkLine(index + 1, lines, raw_html) || | ||
checkLine(index - 1, lines, raw_html) || | ||
checkLine(index - 3, lines, raw_html))) { | ||
return false; | ||
} | ||
if (line === '' && (checkLine(index + 1, lines, dashes) || checkLine(index - 1, lines, dashes))) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
function postprocessRSTParagraph(par) { | ||
let lines = (0, util_1.splitLines)(par.trim()); | ||
lines = lines.map((line, index) => modifyLine(index, line, lines) ? removeBackslashSpace(line.trim().replace('\t', ' ')) : line); | ||
return lines.filter((line, index) => (modifyLine(index, line, lines) ? !!line : true)).join('\n'); | ||
} | ||
function formatAntsibullOptionLike(part, role) { | ||
@@ -76,6 +167,10 @@ const result = []; | ||
formatItalic: (part) => `\\ :emphasis:\`${quoteRST(part.text, true, true, true)}\`\\ `, | ||
formatLink: (part) => (part.text === '' ? '' : `\\ \`${quoteRST(part.text)} <${encodeURI(part.url)}>\`__\\ `), | ||
formatLink: (part) => part.text === '' | ||
? '' | ||
: part.url === '' | ||
? quoteRST(part.text) | ||
: `\\ \`${quoteRST(part.text)} <${encodeURI(part.url)}>\`__\\ `, | ||
formatModule: (part) => `\\ :ref:\`${quoteRST(part.fqcn, true, true, true)} <ansible_collections.${part.fqcn}_module>\`\\ `, | ||
formatRSTRef: (part) => `\\ :ref:\`${quoteRST(part.text, true, true, true)} <${part.ref}>\`\\ `, | ||
formatURL: (part) => `\\ ${encodeURI(part.url)}\\ `, | ||
formatURL: (part) => (part.url === '' ? '' : `\\ \`${quoteRST(part.url)} <${encodeURI(part.url)}>\`__\\ `), | ||
formatText: (part) => quoteRST(part.text), | ||
@@ -94,6 +189,10 @@ formatEnvVariable: (part) => `\\ :envvar:\`${quoteRST(part.name, true, true, true)}\`\\ `, | ||
formatItalic: (part) => `\\ :emphasis:\`${quoteRST(part.text, true, true, true)}\`\\ `, | ||
formatLink: (part) => (part.text === '' ? '' : `\\ \`${quoteRST(part.text)} <${encodeURI(part.url)}>\`__\\ `), | ||
formatLink: (part) => part.text === '' | ||
? '' | ||
: part.url === '' | ||
? quoteRST(part.text) | ||
: `\\ \`${quoteRST(part.text)} <${encodeURI(part.url)}>\`__\\ `, | ||
formatModule: (part) => `\\ :ref:\`${quoteRST(part.fqcn, true, true, true)} <ansible_collections.${part.fqcn}_module>\`\\ `, | ||
formatRSTRef: (part) => `\\ :ref:\`${quoteRST(part.text, true, true, true)} <${part.ref}>\`\\ `, | ||
formatURL: (part) => `\\ ${encodeURI(part.url)}\\ `, | ||
formatURL: (part) => (part.url === '' ? '' : `\\ \`${quoteRST(part.url)} <${encodeURI(part.url)}>\`__\\ `), | ||
formatText: (part) => quoteRST(part.text), | ||
@@ -114,6 +213,4 @@ formatEnvVariable: (part) => `\\ :envvar:\`${quoteRST(part.name, true, true, true)}\`\\ `, | ||
(0, format_1.addToDestination)(line, paragraph, mergedOpts); | ||
if (!line.length) { | ||
line.push('\\ '); | ||
} | ||
result.push(line.join('')); | ||
const lineStr = postprocessRSTParagraph(line.join('')); | ||
result.push(lineStr || '\\'); | ||
} | ||
@@ -120,0 +217,0 @@ return result.join('\n\n'); |
@@ -41,3 +41,3 @@ /* | ||
formatRSTRef: (part) => `<span>${quoteHTML(part.text)}</span>`, | ||
formatURL: (part) => `<a href='${quoteHTMLArg(encodeURI(part.url))}'>${quoteHTML(encodeURI(part.url))}</a>`, | ||
formatURL: (part) => `<a href='${quoteHTMLArg(encodeURI(part.url))}'>${quoteHTML(part.url)}</a>`, | ||
formatText: (part) => quoteHTML(part.text), | ||
@@ -95,3 +95,3 @@ formatEnvVariable: (part) => `<code>${quoteHTML(part.name)}</code>`, | ||
formatRSTRef: (part) => `<span class='module'>${quoteHTML(part.text)}</span>`, | ||
formatURL: (part) => `<a href='${quoteHTMLArg(encodeURI(part.url))}'>${quoteHTML(encodeURI(part.url))}</a>`, | ||
formatURL: (part) => `<a href='${quoteHTMLArg(encodeURI(part.url))}'>${quoteHTML(part.url)}</a>`, | ||
formatText: (part) => quoteHTML(part.text), | ||
@@ -98,0 +98,0 @@ formatEnvVariable: (part) => `<code class="xref std std-envvar literal notranslate">${quoteHTML(part.name)}</code>`, |
import { MDOptions } from './opts'; | ||
import { Paragraph } from './dom'; | ||
export declare function quoteMD(text: string): string; | ||
export declare function postprocessMDParagraph(par: string): string; | ||
export declare function toMD(paragraphs: Paragraph[], opts?: MDOptions): string; | ||
//# sourceMappingURL=md.d.ts.map |
@@ -10,5 +10,12 @@ /* | ||
import { addToDestination } from './format'; | ||
import { splitLines } from './util'; | ||
export function quoteMD(text) { | ||
return text.replace(/([!"#$%&'()*+,:;<=>?@[\\\]^_`{|}~.-])/g, '\\$1'); | ||
} | ||
export function postprocessMDParagraph(par) { | ||
return splitLines(par.trim()) | ||
.map((line) => line.trim().replace('\t', ' ')) | ||
.filter(Boolean) | ||
.join('\n'); | ||
} | ||
function formatOptionLike(part, url, what) { | ||
@@ -47,3 +54,3 @@ let link_start = ''; | ||
formatRSTRef: (part) => `${quoteMD(part.text)}`, | ||
formatURL: (part) => `[${quoteMD(encodeURI(part.url))}](${quoteMD(encodeURI(part.url))})`, | ||
formatURL: (part) => `[${quoteMD(part.url)}](${quoteMD(encodeURI(part.url))})`, | ||
formatText: (part) => quoteMD(part.text), | ||
@@ -62,6 +69,4 @@ formatEnvVariable: (part) => `<code>${quoteMD(part.name)}</code>`, | ||
addToDestination(line, paragraph, mergedOpts); | ||
if (!line.length) { | ||
line.push(' '); | ||
} | ||
result.push(line.join('')); | ||
const lineStr = postprocessMDParagraph(line.join('')); | ||
result.push(lineStr || ' '); | ||
} | ||
@@ -68,0 +73,0 @@ return result.join('\n\n'); |
@@ -17,2 +17,3 @@ import { TextPart, ItalicPart, BoldPart, ModulePart, PluginPart, URLPart, LinkPart, RSTRefPart, CodePart, OptionNamePart, OptionValuePart, EnvVariablePart, ReturnValuePart, HorizontalLinePart, ErrorPart } from './dom'; | ||
} | ||
export type Whitespace = 'ignore' | 'strip' | 'keep_single_newlines'; | ||
export interface ParsingOptions extends ErrorHandlingOptions { | ||
@@ -29,2 +30,14 @@ /** Should be provided if parsing documentation of a plugin/module/role. */ | ||
helpfulErrors?: boolean; | ||
/** | ||
How to handle whitespace (default is 'ignore'). | ||
'ignore': Keep all whitespace as-is. | ||
'strip': Reduce all whitespace (space, tabs, newlines, ...) to regular breakable or | ||
non-breakable spaces. Multiple spaces are kept in everything that's often rendered | ||
code-style, like C(), O(), V(), RV(), E(). | ||
'keep_single_newlines': Similar to 'strip', but keep single newlines intact. | ||
*/ | ||
whitespace?: Whitespace; | ||
} | ||
@@ -31,0 +44,0 @@ export interface CommonExportOptions extends ErrorHandlingOptions { |
@@ -29,3 +29,2 @@ /* | ||
const value = []; | ||
/* eslint-disable-next-line no-constant-condition */ | ||
while (true) { | ||
@@ -55,3 +54,2 @@ escapeOrComma.lastIndex = index; | ||
const value = []; | ||
/* eslint-disable-next-line no-constant-condition */ | ||
while (true) { | ||
@@ -58,0 +56,0 @@ escapeOrClosing.lastIndex = index; |
@@ -1,3 +0,4 @@ | ||
import { ParsingOptions } from './opts'; | ||
import { ParsingOptions, Whitespace } from './opts'; | ||
import { AnyPart, Paragraph } from './dom'; | ||
export declare function processWhitespace(text: string, whitespace: Whitespace, codeEnvironment: boolean, noNewlines: boolean): string; | ||
export interface CommandParser { | ||
@@ -7,3 +8,4 @@ command: string; | ||
escapedArguments?: boolean; | ||
process: (args: string[], opts: ParsingOptions, source: string | undefined) => AnyPart; | ||
stripSurroundingWhitespace?: boolean; | ||
process: (args: string[], opts: ParsingOptions, source: string | undefined, whitespace: Whitespace) => AnyPart; | ||
} | ||
@@ -10,0 +12,0 @@ export declare function composeCommandMap(commands: CommandParser[]): Map<string, CommandParser>; |
@@ -9,2 +9,5 @@ /* | ||
import { PartType, } from './dom'; | ||
function repr(text) { | ||
return JSON.stringify(text); | ||
} | ||
const IGNORE_MARKER = 'ignore:'; | ||
@@ -25,6 +28,6 @@ function parseOptionLike(text, opts, type, source) { | ||
if (!isFQCN(pluginFqcn)) { | ||
throw Error(`Plugin name "${pluginFqcn}" is not a FQCN`); | ||
throw Error(`Plugin name ${repr(pluginFqcn)} is not a FQCN`); | ||
} | ||
if (!isPluginType(pluginType)) { | ||
throw Error(`Plugin type "${pluginType}" is not valid`); | ||
throw Error(`Plugin type ${repr(pluginType)} is not valid`); | ||
} | ||
@@ -53,3 +56,3 @@ plugin = { fqcn: pluginFqcn, type: pluginType }; | ||
if (/[:#]/.test(text)) { | ||
throw Error(`Invalid option/return value name "${text}"`); | ||
throw Error(`Invalid option/return value name ${repr(text)}`); | ||
} | ||
@@ -66,2 +69,58 @@ return { | ||
} | ||
function addWhitespace(result, ws, whitespace, noNewlines) { | ||
if (whitespace === 'keep_single_newlines' && !noNewlines && ws.search(/[\n\r]/) >= 0) { | ||
result.push('\n'); | ||
} | ||
else { | ||
result.push(' '); | ||
} | ||
} | ||
export function processWhitespace(text, whitespace, codeEnvironment, noNewlines) { | ||
if (whitespace === 'ignore') { | ||
return text; | ||
} | ||
const length = text.length; | ||
let index = 0; | ||
const result = []; | ||
const whitespaces = /([\s]+)/g; | ||
// The 'no-misleading-character-class' warning reported by eslint below can be safely | ||
// ignored since we have a list of distinct Unicode codepoints, and we don't rely on | ||
// them being part of the adjacent codepoints. | ||
/* eslint-disable-next-line no-misleading-character-class */ | ||
const spacesToKeep = /([\u00A0\u202F\u2007\u2060\u200B\u200C\u200D\uFEFF]+)/g; | ||
while (index < length) { | ||
whitespaces.lastIndex = index; | ||
const m = whitespaces.exec(text); | ||
if (!m) { | ||
result.push(text.slice(index)); | ||
break; | ||
} | ||
if (m.index > index) { | ||
result.push(text.slice(index, m.index)); | ||
} | ||
const ws = m[0]; | ||
const ws_length = ws.length; | ||
if (codeEnvironment) { | ||
result.push(ws.replace(/[\t\n\r]/g, ' ')); | ||
} | ||
else { | ||
let ws_index = 0; | ||
while (ws_index < ws_length) { | ||
spacesToKeep.lastIndex = ws_index; | ||
const wsm = spacesToKeep.exec(ws); | ||
if (!wsm) { | ||
addWhitespace(result, ws.slice(ws_index), whitespace, noNewlines); | ||
break; | ||
} | ||
if (wsm.index > ws_index) { | ||
addWhitespace(result, ws.slice(ws_index, wsm.index), whitespace, noNewlines); | ||
} | ||
result.push(wsm[0]); | ||
ws_index = wsm.index + wsm[0].length; | ||
} | ||
} | ||
index = m.index + ws_length; | ||
} | ||
return result.join(''); | ||
} | ||
const PARSER = [ | ||
@@ -73,4 +132,4 @@ // Classic Ansible docs markup: | ||
old_markup: true, | ||
process: (args, _, source) => { | ||
const text = args[0]; | ||
process: (args, _, source, whitespace) => { | ||
const text = processWhitespace(args[0], whitespace, false, true); | ||
return { type: PartType.ITALIC, text: text, source: source }; | ||
@@ -83,4 +142,4 @@ }, | ||
old_markup: true, | ||
process: (args, _, source) => { | ||
const text = args[0]; | ||
process: (args, _, source, whitespace) => { | ||
const text = processWhitespace(args[0], whitespace, false, true); | ||
return { type: PartType.BOLD, text: text, source: source }; | ||
@@ -93,6 +152,6 @@ }, | ||
old_markup: true, | ||
process: (args, _, source) => { | ||
const fqcn = args[0]; | ||
process: (args, _, source, whitespace) => { | ||
const fqcn = processWhitespace(args[0], whitespace, false, true); | ||
if (!isFQCN(fqcn)) { | ||
throw Error(`Module name "${fqcn}" is not a FQCN`); | ||
throw Error(`Module name ${repr(fqcn)} is not a FQCN`); | ||
} | ||
@@ -106,4 +165,4 @@ return { type: PartType.MODULE, fqcn: fqcn, source: source }; | ||
old_markup: true, | ||
process: (args, _, source) => { | ||
const url = args[0]; | ||
process: (args, _, source, whitespace) => { | ||
const url = processWhitespace(args[0], whitespace, false, true); | ||
return { type: PartType.URL, url: url, source: source }; | ||
@@ -116,5 +175,5 @@ }, | ||
old_markup: true, | ||
process: (args, _, source) => { | ||
const text = args[0]; | ||
const url = args[1]; | ||
process: (args, _, source, whitespace) => { | ||
const text = processWhitespace(args[0], whitespace, false, true); | ||
const url = processWhitespace(args[1], whitespace, false, true); | ||
return { type: PartType.LINK, text: text, url: url, source: source }; | ||
@@ -127,5 +186,5 @@ }, | ||
old_markup: true, | ||
process: (args, _, source) => { | ||
const text = args[0]; | ||
const ref = args[1]; | ||
process: (args, _, source, whitespace) => { | ||
const text = processWhitespace(args[0], whitespace, false, true); | ||
const ref = processWhitespace(args[1], whitespace, false, true); | ||
return { type: PartType.RST_REF, text: text, ref: ref, source: source }; | ||
@@ -138,4 +197,4 @@ }, | ||
old_markup: true, | ||
process: (args, _, source) => { | ||
const text = args[0]; | ||
process: (args, _, source, whitespace) => { | ||
const text = processWhitespace(args[0], whitespace, true, true); | ||
return { type: PartType.CODE, text: text, source: source }; | ||
@@ -147,2 +206,3 @@ }, | ||
parameters: 0, | ||
stripSurroundingWhitespace: true, | ||
old_markup: true, | ||
@@ -158,14 +218,15 @@ process: (_, __, source) => { | ||
escapedArguments: true, | ||
process: (args, _, source) => { | ||
const m = /^([^#]*)#(.*)$/.exec(args[0]); | ||
process: (args, _, source, whitespace) => { | ||
const plugin = processWhitespace(args[0], whitespace, false, true); | ||
const m = /^([^#]*)#(.*)$/.exec(plugin); | ||
if (!m) { | ||
throw Error(`Parameter "${args[0]}" is not of the form FQCN#type`); | ||
throw Error(`Parameter ${repr(args[0])} is not of the form FQCN#type`); | ||
} | ||
const fqcn = m[1]; | ||
if (!isFQCN(fqcn)) { | ||
throw Error(`Plugin name "${fqcn}" is not a FQCN`); | ||
throw Error(`Plugin name ${repr(fqcn)} is not a FQCN`); | ||
} | ||
const type = m[2]; | ||
if (!isPluginType(type)) { | ||
throw Error(`Plugin type "${type}" is not valid`); | ||
throw Error(`Plugin type ${repr(type)} is not valid`); | ||
} | ||
@@ -179,4 +240,4 @@ return { type: PartType.PLUGIN, plugin: { fqcn: fqcn, type: type }, source: source }; | ||
escapedArguments: true, | ||
process: (args, _, source) => { | ||
const env = args[0]; | ||
process: (args, _, source, whitespace) => { | ||
const env = processWhitespace(args[0], whitespace, true, true); | ||
return { type: PartType.ENV_VARIABLE, name: env, source: source }; | ||
@@ -189,4 +250,4 @@ }, | ||
escapedArguments: true, | ||
process: (args, _, source) => { | ||
const value = args[0]; | ||
process: (args, _, source, whitespace) => { | ||
const value = processWhitespace(args[0], whitespace, true, true); | ||
return { type: PartType.OPTION_VALUE, value: value, source: source }; | ||
@@ -199,4 +260,4 @@ }, | ||
escapedArguments: true, | ||
process: (args, opts, source) => { | ||
const value = args[0]; | ||
process: (args, opts, source, whitespace) => { | ||
const value = processWhitespace(args[0], whitespace, true, true); | ||
return parseOptionLike(value, opts, PartType.OPTION_NAME, source); | ||
@@ -209,4 +270,4 @@ }, | ||
escapedArguments: true, | ||
process: (args, opts, source) => { | ||
const value = args[0]; | ||
process: (args, opts, source, whitespace) => { | ||
const value = processWhitespace(args[0], whitespace, true, true); | ||
return parseOptionLike(value, opts, PartType.RETURN_VALUE, source); | ||
@@ -232,3 +293,5 @@ }, | ||
var _a; | ||
// TODO: handle opts.whitespace! | ||
const result = []; | ||
const whitespace = opts.whitespace || 'ignore'; | ||
const length = input.length; | ||
@@ -244,3 +307,3 @@ let index = 0; | ||
type: PartType.TEXT, | ||
text: text, | ||
text: processWhitespace(text, whitespace, false, false), | ||
source: opts.addSource ? text : undefined, | ||
@@ -251,10 +314,3 @@ }); | ||
} | ||
if (match.index > index) { | ||
const text = input.slice(index, match.index); | ||
result.push({ | ||
type: PartType.TEXT, | ||
text: text, | ||
source: opts.addSource ? text : undefined, | ||
}); | ||
} | ||
const prevIndex = index; | ||
index = match.index; | ||
@@ -268,4 +324,21 @@ let cmd = match[0]; | ||
if (!command) { | ||
throw Error(`Internal error: unknown command "${cmd}"`); | ||
throw Error(`Internal error: unknown command ${repr(cmd)}`); | ||
} | ||
if (match.index > prevIndex) { | ||
let text = input.slice(prevIndex, match.index); | ||
if (command.stripSurroundingWhitespace) { | ||
let end = text.length; | ||
while (end > 0 && /[ \t]/.test(text[end - 1])) { | ||
end -= 1; | ||
} | ||
if (end < text.length) { | ||
text = text.slice(0, end); | ||
} | ||
} | ||
result.push({ | ||
type: PartType.TEXT, | ||
text: processWhitespace(text, whitespace, false, false), | ||
source: opts.addSource ? text : undefined, | ||
}); | ||
} | ||
let args; | ||
@@ -285,3 +358,3 @@ let error; | ||
try { | ||
result.push(command.process(args, opts, source)); | ||
result.push(command.process(args, opts, source, whitespace)); | ||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ | ||
@@ -298,3 +371,3 @@ } | ||
const errorSource = ((_a = opts.helpfulErrors) !== null && _a !== void 0 ? _a : true) | ||
? `"${input.slice(index, endIndex)}"` | ||
? repr(input.slice(index, endIndex)) | ||
: `${cmd}${command.parameters > 0 ? '()' : ''}`; | ||
@@ -317,2 +390,7 @@ error = `While parsing ${errorSource} at index ${match.index + 1}${where}: ${error}`; | ||
index = endIndex; | ||
if (command.stripSurroundingWhitespace) { | ||
while (index < length && /[ \t]/.test(input[index])) { | ||
index += 1; | ||
} | ||
} | ||
} | ||
@@ -319,0 +397,0 @@ return result; |
import { RSTOptions } from './opts'; | ||
import { Paragraph } from './dom'; | ||
export declare function quoteRST(text: string, escape_starting_whitespace?: boolean, escape_ending_whitespace?: boolean, must_not_be_empty?: boolean): string; | ||
export declare function postprocessRSTParagraph(par: string): string; | ||
export declare function toRST(paragraphs: Paragraph[], opts?: RSTOptions): string; | ||
//# sourceMappingURL=rst.d.ts.map |
@@ -8,8 +8,9 @@ /* | ||
import { addToDestination } from './format'; | ||
import { splitLines, startsWith, endsWith } from './util'; | ||
export function quoteRST(text, escape_starting_whitespace = false, escape_ending_whitespace = false, must_not_be_empty = false) { | ||
text = text.replace(/([\\<>_*`])/g, '\\$1'); | ||
if (escape_ending_whitespace && text.endsWith(' ')) { | ||
if (escape_ending_whitespace && /\s$/.test(text.substring(text.length - 1))) { | ||
text = text + '\\ '; | ||
} | ||
if (escape_starting_whitespace && text.startsWith(' ')) { | ||
if (escape_starting_whitespace && /^\s/.test(text)) { | ||
text = '\\ ' + text; | ||
@@ -22,2 +23,91 @@ } | ||
} | ||
function removeBackslashSpace(line) { | ||
let start = 0; | ||
let end = line.length; | ||
while (true) { | ||
// Remove starting '\ '. These have no effect. | ||
while (startsWith(line, '\\ ', start, end)) { | ||
start += 2; | ||
} | ||
// If the line now starts with regular whitespace, trim it. | ||
if (startsWith(line, ' ', start, end)) { | ||
start += 1; | ||
} | ||
else { | ||
// If there is none, we're done. | ||
break; | ||
} | ||
// Remove more starting whitespace, and then check again for leading '\ ' etc. | ||
while (startsWith(line, ' ', start, end)) { | ||
start += 1; | ||
} | ||
} | ||
while (true) { | ||
/* | ||
Remove trailing '\ ' resp. '\' (after line.trim()). These actually have an effect, | ||
since they remove the linebreak. *But* if our markup generator emits '\ ' followed | ||
by a line break, we still want the line break to count, so this is actually fixing | ||
a bug. | ||
*/ | ||
if (endsWith(line, '\\', start, end)) { | ||
end -= 1; | ||
} | ||
while (endsWith(line, '\\ ', start, end)) { | ||
end -= 2; | ||
} | ||
// If the line now ends with regular whitespace, trim it. | ||
if (endsWith(line, ' ', start, end)) { | ||
end -= 1; | ||
} | ||
else { | ||
// If there is none, we're done. | ||
break; | ||
} | ||
// Remove more ending whitespace, and then check again for trailing '\' etc. | ||
while (endsWith(line, ' ', start, end)) { | ||
end -= 1; | ||
} | ||
} | ||
// Return subset of the line | ||
line = line.substring(start, end); | ||
line = line.replace(/\\ (?:\\ )+/g, '\\ '); | ||
line = line.replace(/(?<![\\])([ ])\\ (?![`])/g, '$1'); | ||
line = line.replace(/(?<!:`)\\ ([ .])/g, '$1'); | ||
return line; | ||
} | ||
function checkLine(index, lines, line) { | ||
if (index < 0 || index >= lines.length) { | ||
return false; | ||
} | ||
return lines[index] === line; | ||
} | ||
function modifyLine(index, line, lines) { | ||
const raw_html = '.. raw:: html'; | ||
const dashes = '------------'; | ||
const hr = ' <hr>'; | ||
if (line !== '' && line !== raw_html && line !== dashes && line !== hr) { | ||
return true; | ||
} | ||
if (line === raw_html || line === dashes) { | ||
return false; | ||
} | ||
if (line === hr && checkLine(index - 2, lines, raw_html)) { | ||
return false; | ||
} | ||
if (line === '' && | ||
(checkLine(index + 1, lines, raw_html) || | ||
checkLine(index - 1, lines, raw_html) || | ||
checkLine(index - 3, lines, raw_html))) { | ||
return false; | ||
} | ||
if (line === '' && (checkLine(index + 1, lines, dashes) || checkLine(index - 1, lines, dashes))) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
export function postprocessRSTParagraph(par) { | ||
let lines = splitLines(par.trim()); | ||
lines = lines.map((line, index) => modifyLine(index, line, lines) ? removeBackslashSpace(line.trim().replace('\t', ' ')) : line); | ||
return lines.filter((line, index) => (modifyLine(index, line, lines) ? !!line : true)).join('\n'); | ||
} | ||
function formatAntsibullOptionLike(part, role) { | ||
@@ -72,6 +162,10 @@ const result = []; | ||
formatItalic: (part) => `\\ :emphasis:\`${quoteRST(part.text, true, true, true)}\`\\ `, | ||
formatLink: (part) => (part.text === '' ? '' : `\\ \`${quoteRST(part.text)} <${encodeURI(part.url)}>\`__\\ `), | ||
formatLink: (part) => part.text === '' | ||
? '' | ||
: part.url === '' | ||
? quoteRST(part.text) | ||
: `\\ \`${quoteRST(part.text)} <${encodeURI(part.url)}>\`__\\ `, | ||
formatModule: (part) => `\\ :ref:\`${quoteRST(part.fqcn, true, true, true)} <ansible_collections.${part.fqcn}_module>\`\\ `, | ||
formatRSTRef: (part) => `\\ :ref:\`${quoteRST(part.text, true, true, true)} <${part.ref}>\`\\ `, | ||
formatURL: (part) => `\\ ${encodeURI(part.url)}\\ `, | ||
formatURL: (part) => (part.url === '' ? '' : `\\ \`${quoteRST(part.url)} <${encodeURI(part.url)}>\`__\\ `), | ||
formatText: (part) => quoteRST(part.text), | ||
@@ -90,6 +184,10 @@ formatEnvVariable: (part) => `\\ :envvar:\`${quoteRST(part.name, true, true, true)}\`\\ `, | ||
formatItalic: (part) => `\\ :emphasis:\`${quoteRST(part.text, true, true, true)}\`\\ `, | ||
formatLink: (part) => (part.text === '' ? '' : `\\ \`${quoteRST(part.text)} <${encodeURI(part.url)}>\`__\\ `), | ||
formatLink: (part) => part.text === '' | ||
? '' | ||
: part.url === '' | ||
? quoteRST(part.text) | ||
: `\\ \`${quoteRST(part.text)} <${encodeURI(part.url)}>\`__\\ `, | ||
formatModule: (part) => `\\ :ref:\`${quoteRST(part.fqcn, true, true, true)} <ansible_collections.${part.fqcn}_module>\`\\ `, | ||
formatRSTRef: (part) => `\\ :ref:\`${quoteRST(part.text, true, true, true)} <${part.ref}>\`\\ `, | ||
formatURL: (part) => `\\ ${encodeURI(part.url)}\\ `, | ||
formatURL: (part) => (part.url === '' ? '' : `\\ \`${quoteRST(part.url)} <${encodeURI(part.url)}>\`__\\ `), | ||
formatText: (part) => quoteRST(part.text), | ||
@@ -110,6 +208,4 @@ formatEnvVariable: (part) => `\\ :envvar:\`${quoteRST(part.name, true, true, true)}\`\\ `, | ||
addToDestination(line, paragraph, mergedOpts); | ||
if (!line.length) { | ||
line.push('\\ '); | ||
} | ||
result.push(line.join('')); | ||
const lineStr = postprocessRSTParagraph(line.join('')); | ||
result.push(lineStr || '\\'); | ||
} | ||
@@ -116,0 +212,0 @@ return result.join('\n\n'); |
{ | ||
"name": "antsibull-docs", | ||
"version": "1.0.2", | ||
"version": "1.1.0", | ||
"description": "TypeScript library for processing Ansible documentation markup", | ||
@@ -54,13 +54,16 @@ "main": "dist/cjs/index.js", | ||
"devDependencies": { | ||
"@eslint/eslintrc": "^3.1.0", | ||
"@eslint/js": "^9.8.0", | ||
"@types/jest": "^29.5.12", | ||
"@typescript-eslint/eslint-plugin": "^7.6.0", | ||
"@typescript-eslint/parser": "^7.6.0", | ||
"eslint": "^8.56.0", | ||
"@typescript-eslint/eslint-plugin": "^8.0.0", | ||
"@typescript-eslint/parser": "^8.0.0", | ||
"eslint": "^9.8.0", | ||
"globals": "^15.9.0", | ||
"jest": "^29.7.0", | ||
"prettier": "^3.2.5", | ||
"ts-jest": "^29.1.2", | ||
"prettier": "^3.3.3", | ||
"ts-jest": "^29.2.4", | ||
"ts-node": "^10.9.2", | ||
"typescript": "^5.4.5", | ||
"yaml": "^2.4.1" | ||
"typescript": "^5.5.4", | ||
"yaml": "^2.5.0" | ||
} | ||
} |
@@ -52,3 +52,3 @@ /* | ||
formatRSTRef: (part) => `<span>${quoteHTML(part.text)}</span>`, | ||
formatURL: (part) => `<a href='${quoteHTMLArg(encodeURI(part.url))}'>${quoteHTML(encodeURI(part.url))}</a>`, | ||
formatURL: (part) => `<a href='${quoteHTMLArg(encodeURI(part.url))}'>${quoteHTML(part.url)}</a>`, | ||
formatText: (part) => quoteHTML(part.text), | ||
@@ -115,3 +115,3 @@ formatEnvVariable: (part) => `<code>${quoteHTML(part.name)}</code>`, | ||
formatRSTRef: (part) => `<span class='module'>${quoteHTML(part.text)}</span>`, | ||
formatURL: (part) => `<a href='${quoteHTMLArg(encodeURI(part.url))}'>${quoteHTML(encodeURI(part.url))}</a>`, | ||
formatURL: (part) => `<a href='${quoteHTMLArg(encodeURI(part.url))}'>${quoteHTML(part.url)}</a>`, | ||
formatText: (part) => quoteHTML(part.text), | ||
@@ -118,0 +118,0 @@ formatEnvVariable: (part) => `<code class="xref std std-envvar literal notranslate">${quoteHTML(part.name)}</code>`, |
@@ -7,3 +7,3 @@ /* | ||
import { quoteMD, toMD } from './md'; | ||
import { quoteMD, toMD, postprocessMDParagraph } from './md'; | ||
import { PartType } from './dom'; | ||
@@ -25,2 +25,11 @@ | ||
describe('postprocessMDParagraph tests', (): void => { | ||
it('empty string', (): void => { | ||
expect(postprocessMDParagraph('')).toBe(''); | ||
}); | ||
it('string with some whitespace', (): void => { | ||
expect(postprocessMDParagraph(' \n foo \n\r\n \n\tbar \n ')).toBe('foo\nbar'); | ||
}); | ||
}); | ||
describe('toMD tests', (): void => { | ||
@@ -27,0 +36,0 @@ it('no paragraphs', (): void => { |
@@ -13,2 +13,3 @@ /* | ||
import { addToDestination } from './format'; | ||
import { splitLines } from './util'; | ||
@@ -19,2 +20,9 @@ export function quoteMD(text: string): string { | ||
export function postprocessMDParagraph(par: string): string { | ||
return splitLines(par.trim()) | ||
.map((line) => line.trim().replace('\t', ' ')) | ||
.filter(Boolean) | ||
.join('\n'); | ||
} | ||
function formatOptionLike( | ||
@@ -57,3 +65,3 @@ part: OptionNamePart | ReturnValuePart, | ||
formatRSTRef: (part) => `${quoteMD(part.text)}`, | ||
formatURL: (part) => `[${quoteMD(encodeURI(part.url))}](${quoteMD(encodeURI(part.url))})`, | ||
formatURL: (part) => `[${quoteMD(part.url)}](${quoteMD(encodeURI(part.url))})`, | ||
formatText: (part) => quoteMD(part.text), | ||
@@ -74,8 +82,6 @@ formatEnvVariable: (part) => `<code>${quoteMD(part.name)}</code>`, | ||
addToDestination(line, paragraph, mergedOpts); | ||
if (!line.length) { | ||
line.push(' '); | ||
} | ||
result.push(line.join('')); | ||
const lineStr = postprocessMDParagraph(line.join('')); | ||
result.push(lineStr || ' '); | ||
} | ||
return result.join('\n\n'); | ||
} |
@@ -42,2 +42,4 @@ /* | ||
export type Whitespace = 'ignore' | 'strip' | 'keep_single_newlines'; | ||
export interface ParsingOptions extends ErrorHandlingOptions { | ||
@@ -58,2 +60,15 @@ /** Should be provided if parsing documentation of a plugin/module/role. */ | ||
helpfulErrors?: boolean; | ||
/** | ||
How to handle whitespace (default is 'ignore'). | ||
'ignore': Keep all whitespace as-is. | ||
'strip': Reduce all whitespace (space, tabs, newlines, ...) to regular breakable or | ||
non-breakable spaces. Multiple spaces are kept in everything that's often rendered | ||
code-style, like C(), O(), V(), RV(), E(). | ||
'keep_single_newlines': Similar to 'strip', but keep single newlines intact. | ||
*/ | ||
whitespace?: Whitespace; | ||
} | ||
@@ -60,0 +75,0 @@ |
@@ -32,3 +32,2 @@ /* | ||
const value: string[] = []; | ||
/* eslint-disable-next-line no-constant-condition */ | ||
while (true) { | ||
@@ -58,3 +57,2 @@ escapeOrComma.lastIndex = index; | ||
const value: string[] = []; | ||
/* eslint-disable-next-line no-constant-condition */ | ||
while (true) { | ||
@@ -61,0 +59,0 @@ escapeOrClosing.lastIndex = index; |
@@ -7,5 +7,33 @@ /* | ||
import { parse, composeCommandMap, composeCommandRE, CommandParser, parseString } from './parser'; | ||
import { processWhitespace, parse, composeCommandMap, composeCommandRE, CommandParser, parseString } from './parser'; | ||
import { PartType } from './dom'; | ||
describe('processWhitespace', (): void => { | ||
it('empty', (): void => { | ||
expect(processWhitespace('', 'strip', false, false)).toEqual(''); | ||
expect(processWhitespace('', 'keep_single_newlines', false, false)).toEqual(''); | ||
}); | ||
it('one space', (): void => { | ||
expect(processWhitespace(' ', 'strip', false, false)).toEqual(' '); | ||
expect(processWhitespace(' ', 'keep_single_newlines', false, false)).toEqual(' '); | ||
}); | ||
it('two spaces', (): void => { | ||
expect(processWhitespace(' ', 'strip', false, false)).toEqual(' '); | ||
expect(processWhitespace(' ', 'keep_single_newlines', false, false)).toEqual(' '); | ||
}); | ||
it('newline', (): void => { | ||
expect(processWhitespace('\n', 'strip', false, false)).toEqual(' '); | ||
expect(processWhitespace('\n', 'keep_single_newlines', false, false)).toEqual('\n'); | ||
}); | ||
it('newline, no newlines allowed', (): void => { | ||
expect(processWhitespace('\n', 'strip', false, true)).toEqual(' '); | ||
expect(processWhitespace('\n', 'keep_single_newlines', false, true)).toEqual(' '); | ||
}); | ||
it('complex', (): void => { | ||
const input = 'Foo \n\r\t\n\r Bar'; | ||
expect(processWhitespace(input, 'strip', false, false)).toEqual('Foo Bar'); | ||
expect(processWhitespace(input, 'keep_single_newlines', false, false)).toEqual('Foo\nBar'); | ||
}); | ||
}); | ||
describe('parser', (): void => { | ||
@@ -40,3 +68,2 @@ it('empty string', (): void => { | ||
{ type: PartType.HORIZONTAL_LINE, source: undefined }, | ||
{ type: PartType.TEXT, text: ' ', source: undefined }, | ||
{ type: PartType.LINK, text: 'foo', url: 'https://bar.com', source: undefined }, | ||
@@ -68,3 +95,2 @@ { type: PartType.TEXT, text: ' ', source: undefined }, | ||
{ type: PartType.HORIZONTAL_LINE, source: 'HORIZONTALLINE' }, | ||
{ type: PartType.TEXT, text: ' ', source: ' ' }, | ||
{ type: PartType.LINK, text: 'foo', url: 'https://bar.com', source: 'L(foo , https://bar.com)' }, | ||
@@ -96,3 +122,2 @@ { type: PartType.TEXT, text: ' ', source: ' ' }, | ||
{ type: PartType.HORIZONTAL_LINE, source: undefined }, | ||
{ type: PartType.TEXT, text: ' ', source: undefined }, | ||
{ type: PartType.LINK, text: 'foo', url: 'https://bar.com', source: undefined }, | ||
@@ -99,0 +124,0 @@ { type: PartType.TEXT, text: ' ', source: undefined }, |
@@ -7,3 +7,3 @@ /* | ||
import { ParsingOptions, PluginIdentifier } from './opts'; | ||
import { ParsingOptions, PluginIdentifier, Whitespace } from './opts'; | ||
import { isFQCN, isPluginType } from './ansible'; | ||
@@ -32,2 +32,6 @@ import { parseEscapedArgs, parseUnescapedArgs } from './parser-impl'; | ||
function repr(text: string): string { | ||
return JSON.stringify(text); | ||
} | ||
const IGNORE_MARKER = 'ignore:'; | ||
@@ -54,6 +58,6 @@ | ||
if (!isFQCN(pluginFqcn)) { | ||
throw Error(`Plugin name "${pluginFqcn}" is not a FQCN`); | ||
throw Error(`Plugin name ${repr(pluginFqcn)} is not a FQCN`); | ||
} | ||
if (!isPluginType(pluginType)) { | ||
throw Error(`Plugin type "${pluginType}" is not valid`); | ||
throw Error(`Plugin type ${repr(pluginType)} is not valid`); | ||
} | ||
@@ -80,3 +84,3 @@ plugin = { fqcn: pluginFqcn, type: pluginType }; | ||
if (/[:#]/.test(text)) { | ||
throw Error(`Invalid option/return value name "${text}"`); | ||
throw Error(`Invalid option/return value name ${repr(text)}`); | ||
} | ||
@@ -94,2 +98,63 @@ return { | ||
function addWhitespace(result: string[], ws: string, whitespace: Whitespace, noNewlines: boolean): void { | ||
if (whitespace === 'keep_single_newlines' && !noNewlines && ws.search(/[\n\r]/) >= 0) { | ||
result.push('\n'); | ||
} else { | ||
result.push(' '); | ||
} | ||
} | ||
export function processWhitespace( | ||
text: string, | ||
whitespace: Whitespace, | ||
codeEnvironment: boolean, | ||
noNewlines: boolean, | ||
): string { | ||
if (whitespace === 'ignore') { | ||
return text; | ||
} | ||
const length = text.length; | ||
let index = 0; | ||
const result: string[] = []; | ||
const whitespaces = /([\s]+)/g; | ||
// The 'no-misleading-character-class' warning reported by eslint below can be safely | ||
// ignored since we have a list of distinct Unicode codepoints, and we don't rely on | ||
// them being part of the adjacent codepoints. | ||
/* eslint-disable-next-line no-misleading-character-class */ | ||
const spacesToKeep = /([\u00A0\u202F\u2007\u2060\u200B\u200C\u200D\uFEFF]+)/g; | ||
while (index < length) { | ||
whitespaces.lastIndex = index; | ||
const m = whitespaces.exec(text); | ||
if (!m) { | ||
result.push(text.slice(index)); | ||
break; | ||
} | ||
if (m.index > index) { | ||
result.push(text.slice(index, m.index)); | ||
} | ||
const ws = m[0]; | ||
const ws_length = ws.length; | ||
if (codeEnvironment) { | ||
result.push(ws.replace(/[\t\n\r]/g, ' ')); | ||
} else { | ||
let ws_index = 0; | ||
while (ws_index < ws_length) { | ||
spacesToKeep.lastIndex = ws_index; | ||
const wsm = spacesToKeep.exec(ws); | ||
if (!wsm) { | ||
addWhitespace(result, ws.slice(ws_index), whitespace, noNewlines); | ||
break; | ||
} | ||
if (wsm.index > ws_index) { | ||
addWhitespace(result, ws.slice(ws_index, wsm.index), whitespace, noNewlines); | ||
} | ||
result.push(wsm[0]); | ||
ws_index = wsm.index + wsm[0].length; | ||
} | ||
} | ||
index = m.index + ws_length; | ||
} | ||
return result.join(''); | ||
} | ||
export interface CommandParser { | ||
@@ -99,3 +164,4 @@ command: string; | ||
escapedArguments?: boolean; | ||
process: (args: string[], opts: ParsingOptions, source: string | undefined) => AnyPart; | ||
stripSurroundingWhitespace?: boolean; | ||
process: (args: string[], opts: ParsingOptions, source: string | undefined, whitespace: Whitespace) => AnyPart; | ||
} | ||
@@ -113,4 +179,4 @@ | ||
old_markup: true, | ||
process: (args, _, source) => { | ||
const text = args[0] as string; | ||
process: (args, _, source, whitespace) => { | ||
const text = processWhitespace(args[0] as string, whitespace, false, true); | ||
return <ItalicPart>{ type: PartType.ITALIC, text: text, source: source }; | ||
@@ -123,4 +189,4 @@ }, | ||
old_markup: true, | ||
process: (args, _, source) => { | ||
const text = args[0] as string; | ||
process: (args, _, source, whitespace) => { | ||
const text = processWhitespace(args[0] as string, whitespace, false, true); | ||
return <BoldPart>{ type: PartType.BOLD, text: text, source: source }; | ||
@@ -133,6 +199,6 @@ }, | ||
old_markup: true, | ||
process: (args, _, source) => { | ||
const fqcn = args[0] as string; | ||
process: (args, _, source, whitespace) => { | ||
const fqcn = processWhitespace(args[0] as string, whitespace, false, true); | ||
if (!isFQCN(fqcn)) { | ||
throw Error(`Module name "${fqcn}" is not a FQCN`); | ||
throw Error(`Module name ${repr(fqcn)} is not a FQCN`); | ||
} | ||
@@ -146,4 +212,4 @@ return <ModulePart>{ type: PartType.MODULE, fqcn: fqcn, source: source }; | ||
old_markup: true, | ||
process: (args, _, source) => { | ||
const url = args[0] as string; | ||
process: (args, _, source, whitespace) => { | ||
const url = processWhitespace(args[0] as string, whitespace, false, true); | ||
return <URLPart>{ type: PartType.URL, url: url, source: source }; | ||
@@ -156,5 +222,5 @@ }, | ||
old_markup: true, | ||
process: (args, _, source) => { | ||
const text = args[0] as string; | ||
const url = args[1] as string; | ||
process: (args, _, source, whitespace) => { | ||
const text = processWhitespace(args[0] as string, whitespace, false, true); | ||
const url = processWhitespace(args[1] as string, whitespace, false, true); | ||
return <LinkPart>{ type: PartType.LINK, text: text, url: url, source: source }; | ||
@@ -167,5 +233,5 @@ }, | ||
old_markup: true, | ||
process: (args, _, source) => { | ||
const text = args[0] as string; | ||
const ref = args[1] as string; | ||
process: (args, _, source, whitespace) => { | ||
const text = processWhitespace(args[0] as string, whitespace, false, true); | ||
const ref = processWhitespace(args[1] as string, whitespace, false, true); | ||
return <RSTRefPart>{ type: PartType.RST_REF, text: text, ref: ref, source: source }; | ||
@@ -178,4 +244,4 @@ }, | ||
old_markup: true, | ||
process: (args, _, source) => { | ||
const text = args[0] as string; | ||
process: (args, _, source, whitespace) => { | ||
const text = processWhitespace(args[0] as string, whitespace, true, true); | ||
return <CodePart>{ type: PartType.CODE, text: text, source: source }; | ||
@@ -187,2 +253,3 @@ }, | ||
parameters: 0, | ||
stripSurroundingWhitespace: true, | ||
old_markup: true, | ||
@@ -198,14 +265,15 @@ process: (_, __, source) => { | ||
escapedArguments: true, | ||
process: (args, _, source) => { | ||
const m = /^([^#]*)#(.*)$/.exec(args[0] as string); | ||
process: (args, _, source, whitespace) => { | ||
const plugin = processWhitespace(args[0] as string, whitespace, false, true); | ||
const m = /^([^#]*)#(.*)$/.exec(plugin); | ||
if (!m) { | ||
throw Error(`Parameter "${args[0]}" is not of the form FQCN#type`); | ||
throw Error(`Parameter ${repr(args[0] as string)} is not of the form FQCN#type`); | ||
} | ||
const fqcn = m[1] as string; | ||
if (!isFQCN(fqcn)) { | ||
throw Error(`Plugin name "${fqcn}" is not a FQCN`); | ||
throw Error(`Plugin name ${repr(fqcn)} is not a FQCN`); | ||
} | ||
const type = m[2] as string; | ||
if (!isPluginType(type)) { | ||
throw Error(`Plugin type "${type}" is not valid`); | ||
throw Error(`Plugin type ${repr(type)} is not valid`); | ||
} | ||
@@ -219,4 +287,4 @@ return <PluginPart>{ type: PartType.PLUGIN, plugin: { fqcn: fqcn, type: type }, source: source }; | ||
escapedArguments: true, | ||
process: (args, _, source) => { | ||
const env = args[0] as string; | ||
process: (args, _, source, whitespace) => { | ||
const env = processWhitespace(args[0] as string, whitespace, true, true); | ||
return <EnvVariablePart>{ type: PartType.ENV_VARIABLE, name: env, source: source }; | ||
@@ -229,4 +297,4 @@ }, | ||
escapedArguments: true, | ||
process: (args, _, source) => { | ||
const value = args[0] as string; | ||
process: (args, _, source, whitespace) => { | ||
const value = processWhitespace(args[0] as string, whitespace, true, true); | ||
return <OptionValuePart>{ type: PartType.OPTION_VALUE, value: value, source: source }; | ||
@@ -239,4 +307,4 @@ }, | ||
escapedArguments: true, | ||
process: (args, opts, source) => { | ||
const value = args[0] as string; | ||
process: (args, opts, source, whitespace) => { | ||
const value = processWhitespace(args[0] as string, whitespace, true, true); | ||
return parseOptionLike(value, opts, PartType.OPTION_NAME, source); | ||
@@ -249,4 +317,4 @@ }, | ||
escapedArguments: true, | ||
process: (args, opts, source) => { | ||
const value = args[0] as string; | ||
process: (args, opts, source, whitespace) => { | ||
const value = processWhitespace(args[0] as string, whitespace, true, true); | ||
return parseOptionLike(value, opts, PartType.RETURN_VALUE, source); | ||
@@ -282,3 +350,5 @@ }, | ||
): Paragraph { | ||
// TODO: handle opts.whitespace! | ||
const result: AnyPart[] = []; | ||
const whitespace = opts.whitespace || 'ignore'; | ||
const length = input.length; | ||
@@ -294,3 +364,3 @@ let index = 0; | ||
type: PartType.TEXT, | ||
text: text, | ||
text: processWhitespace(text, whitespace, false, false), | ||
source: opts.addSource ? text : undefined, | ||
@@ -301,10 +371,3 @@ }); | ||
} | ||
if (match.index > index) { | ||
const text = input.slice(index, match.index); | ||
result.push(<TextPart>{ | ||
type: PartType.TEXT, | ||
text: text, | ||
source: opts.addSource ? text : undefined, | ||
}); | ||
} | ||
const prevIndex = index; | ||
index = match.index; | ||
@@ -318,4 +381,21 @@ let cmd = match[0]; | ||
if (!command) { | ||
throw Error(`Internal error: unknown command "${cmd}"`); | ||
throw Error(`Internal error: unknown command ${repr(cmd)}`); | ||
} | ||
if (match.index > prevIndex) { | ||
let text = input.slice(prevIndex, match.index); | ||
if (command.stripSurroundingWhitespace) { | ||
let end = text.length; | ||
while (end > 0 && /[ \t]/.test(text[end - 1] as string)) { | ||
end -= 1; | ||
} | ||
if (end < text.length) { | ||
text = text.slice(0, end); | ||
} | ||
} | ||
result.push(<TextPart>{ | ||
type: PartType.TEXT, | ||
text: processWhitespace(text, whitespace, false, false), | ||
source: opts.addSource ? text : undefined, | ||
}); | ||
} | ||
let args: string[]; | ||
@@ -333,3 +413,3 @@ let error: string | undefined; | ||
try { | ||
result.push(command.process(args, opts, source)); | ||
result.push(command.process(args, opts, source, whitespace)); | ||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ | ||
@@ -345,4 +425,4 @@ } catch (exc: any) { | ||
const errorSource = | ||
opts.helpfulErrors ?? true | ||
? `"${input.slice(index, endIndex)}"` | ||
(opts.helpfulErrors ?? true) | ||
? repr(input.slice(index, endIndex)) | ||
: `${cmd}${command.parameters > 0 ? '()' : ''}`; | ||
@@ -365,2 +445,7 @@ error = `While parsing ${errorSource} at index ${match.index + 1}${where}: ${error}`; | ||
index = endIndex; | ||
if (command.stripSurroundingWhitespace) { | ||
while (index < length && /[ \t]/.test(input[index] as string)) { | ||
index += 1; | ||
} | ||
} | ||
} | ||
@@ -367,0 +452,0 @@ return result; |
@@ -7,3 +7,3 @@ /* | ||
import { quoteRST, toRST } from './rst'; | ||
import { quoteRST, toRST, postprocessRSTParagraph } from './rst'; | ||
import { PartType } from './dom'; | ||
@@ -35,2 +35,26 @@ | ||
describe('postprocessRSTParagraph tests', (): void => { | ||
it('empty string', (): void => { | ||
expect(postprocessRSTParagraph('')).toBe(''); | ||
}); | ||
it('string with some whitespace', (): void => { | ||
expect(postprocessRSTParagraph(' \n foo \n\r\n \n\tbar \n ')).toBe('foo\nbar'); | ||
}); | ||
it('iteratively collapsing whitespace and escaped spaces', (): void => { | ||
expect(postprocessRSTParagraph('\\ foo\\ \\ bar \\ \\ \n\nf\\ oo')).toBe('foo bar\nf\\ oo'); | ||
}); | ||
it('collapsing multiple escaped spaces', (): void => { | ||
expect(postprocessRSTParagraph('a\\ \\ \\ \\ \\ b')).toBe('a\\ b'); | ||
}); | ||
it('iteratively collapsing whitespace and escaped spaces at end of line', (): void => { | ||
expect(postprocessRSTParagraph('a\\ \\ \\ \\ \\ \\ ')).toBe('a'); | ||
}); | ||
it('iteratively collapsing whitespace and escaped spaces at start of line', (): void => { | ||
expect(postprocessRSTParagraph('\\ \\ \\ \\ \\ \\ a')).toBe('a'); | ||
}); | ||
it('iteratively collapsing whitespace and escaped spaces composing the line', (): void => { | ||
expect(postprocessRSTParagraph('\\ \\ \\ \\ \\ \\ ')).toBe(''); | ||
}); | ||
}); | ||
describe('toRST tests', (): void => { | ||
@@ -37,0 +61,0 @@ it('no paragraphs', (): void => { |
131
src/rst.ts
@@ -10,2 +10,3 @@ /* | ||
import { addToDestination } from './format'; | ||
import { splitLines, startsWith, endsWith } from './util'; | ||
@@ -20,6 +21,6 @@ export function quoteRST( | ||
if (escape_ending_whitespace && text.endsWith(' ')) { | ||
if (escape_ending_whitespace && /\s$/.test(text.substring(text.length - 1))) { | ||
text = text + '\\ '; | ||
} | ||
if (escape_starting_whitespace && text.startsWith(' ')) { | ||
if (escape_starting_whitespace && /^\s/.test(text)) { | ||
text = '\\ ' + text; | ||
@@ -33,2 +34,104 @@ } | ||
function removeBackslashSpace(line: string): string { | ||
let start = 0; | ||
let end = line.length; | ||
while (true) { | ||
// Remove starting '\ '. These have no effect. | ||
while (startsWith(line, '\\ ', start, end)) { | ||
start += 2; | ||
} | ||
// If the line now starts with regular whitespace, trim it. | ||
if (startsWith(line, ' ', start, end)) { | ||
start += 1; | ||
} else { | ||
// If there is none, we're done. | ||
break; | ||
} | ||
// Remove more starting whitespace, and then check again for leading '\ ' etc. | ||
while (startsWith(line, ' ', start, end)) { | ||
start += 1; | ||
} | ||
} | ||
while (true) { | ||
/* | ||
Remove trailing '\ ' resp. '\' (after line.trim()). These actually have an effect, | ||
since they remove the linebreak. *But* if our markup generator emits '\ ' followed | ||
by a line break, we still want the line break to count, so this is actually fixing | ||
a bug. | ||
*/ | ||
if (endsWith(line, '\\', start, end)) { | ||
end -= 1; | ||
} | ||
while (endsWith(line, '\\ ', start, end)) { | ||
end -= 2; | ||
} | ||
// If the line now ends with regular whitespace, trim it. | ||
if (endsWith(line, ' ', start, end)) { | ||
end -= 1; | ||
} else { | ||
// If there is none, we're done. | ||
break; | ||
} | ||
// Remove more ending whitespace, and then check again for trailing '\' etc. | ||
while (endsWith(line, ' ', start, end)) { | ||
end -= 1; | ||
} | ||
} | ||
// Return subset of the line | ||
line = line.substring(start, end); | ||
line = line.replace(/\\ (?:\\ )+/g, '\\ '); | ||
line = line.replace(/(?<![\\])([ ])\\ (?![`])/g, '$1'); | ||
line = line.replace(/(?<!:`)\\ ([ .])/g, '$1'); | ||
return line; | ||
} | ||
function checkLine(index: number, lines: string[], line: string): boolean { | ||
if (index < 0 || index >= lines.length) { | ||
return false; | ||
} | ||
return lines[index] === line; | ||
} | ||
function modifyLine(index: number, line: string, lines: string[]): boolean { | ||
const raw_html = '.. raw:: html'; | ||
const dashes = '------------'; | ||
const hr = ' <hr>'; | ||
if (line !== '' && line !== raw_html && line !== dashes && line !== hr) { | ||
return true; | ||
} | ||
if (line === raw_html || line === dashes) { | ||
return false; | ||
} | ||
if (line === hr && checkLine(index - 2, lines, raw_html)) { | ||
return false; | ||
} | ||
if ( | ||
line === '' && | ||
(checkLine(index + 1, lines, raw_html) || | ||
checkLine(index - 1, lines, raw_html) || | ||
checkLine(index - 3, lines, raw_html)) | ||
) { | ||
return false; | ||
} | ||
if (line === '' && (checkLine(index + 1, lines, dashes) || checkLine(index - 1, lines, dashes))) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
export function postprocessRSTParagraph(par: string): string { | ||
let lines = splitLines(par.trim()); | ||
lines = lines.map((line, index) => | ||
modifyLine(index, line, lines) ? removeBackslashSpace(line.trim().replace('\t', ' ')) : line, | ||
); | ||
return lines.filter((line, index) => (modifyLine(index, line, lines) ? !!line : true)).join('\n'); | ||
} | ||
function formatAntsibullOptionLike(part: OptionNamePart | ReturnValuePart, role: string): string { | ||
@@ -87,7 +190,12 @@ const result: string[] = []; | ||
formatItalic: (part) => `\\ :emphasis:\`${quoteRST(part.text, true, true, true)}\`\\ `, | ||
formatLink: (part) => (part.text === '' ? '' : `\\ \`${quoteRST(part.text)} <${encodeURI(part.url)}>\`__\\ `), | ||
formatLink: (part) => | ||
part.text === '' | ||
? '' | ||
: part.url === '' | ||
? quoteRST(part.text) | ||
: `\\ \`${quoteRST(part.text)} <${encodeURI(part.url)}>\`__\\ `, | ||
formatModule: (part) => | ||
`\\ :ref:\`${quoteRST(part.fqcn, true, true, true)} <ansible_collections.${part.fqcn}_module>\`\\ `, | ||
formatRSTRef: (part) => `\\ :ref:\`${quoteRST(part.text, true, true, true)} <${part.ref}>\`\\ `, | ||
formatURL: (part) => `\\ ${encodeURI(part.url)}\\ `, | ||
formatURL: (part) => (part.url === '' ? '' : `\\ \`${quoteRST(part.url)} <${encodeURI(part.url)}>\`__\\ `), | ||
formatText: (part) => quoteRST(part.text), | ||
@@ -108,7 +216,12 @@ formatEnvVariable: (part) => `\\ :envvar:\`${quoteRST(part.name, true, true, true)}\`\\ `, | ||
formatItalic: (part) => `\\ :emphasis:\`${quoteRST(part.text, true, true, true)}\`\\ `, | ||
formatLink: (part) => (part.text === '' ? '' : `\\ \`${quoteRST(part.text)} <${encodeURI(part.url)}>\`__\\ `), | ||
formatLink: (part) => | ||
part.text === '' | ||
? '' | ||
: part.url === '' | ||
? quoteRST(part.text) | ||
: `\\ \`${quoteRST(part.text)} <${encodeURI(part.url)}>\`__\\ `, | ||
formatModule: (part) => | ||
`\\ :ref:\`${quoteRST(part.fqcn, true, true, true)} <ansible_collections.${part.fqcn}_module>\`\\ `, | ||
formatRSTRef: (part) => `\\ :ref:\`${quoteRST(part.text, true, true, true)} <${part.ref}>\`\\ `, | ||
formatURL: (part) => `\\ ${encodeURI(part.url)}\\ `, | ||
formatURL: (part) => (part.url === '' ? '' : `\\ \`${quoteRST(part.url)} <${encodeURI(part.url)}>\`__\\ `), | ||
formatText: (part) => quoteRST(part.text), | ||
@@ -131,8 +244,6 @@ formatEnvVariable: (part) => `\\ :envvar:\`${quoteRST(part.name, true, true, true)}\`\\ `, | ||
addToDestination(line, paragraph, mergedOpts); | ||
if (!line.length) { | ||
line.push('\\ '); | ||
} | ||
result.push(line.join('')); | ||
const lineStr = postprocessRSTParagraph(line.join('')); | ||
result.push(lineStr || '\\'); | ||
} | ||
return result.join('\n\n'); | ||
} |
@@ -17,2 +17,27 @@ /* | ||
function parseLinkProvider(opts) { | ||
if (opts['pluginLinkTemplate']) { | ||
const template = opts['pluginLinkTemplate']; | ||
opts.pluginLink = (plugin_data) => | ||
template | ||
.replace(/{plugin_fqcn}/g, plugin_data.fqcn) | ||
.replace(/{plugin_fqcn_slashes}/g, plugin_data.fqcn.replace(/\./g, '/')) | ||
.replace(/{plugin_type}/g, plugin_data.type); | ||
} | ||
if (opts['pluginOptionLikeLinkTemplate']) { | ||
const template = opts['pluginOptionLikeLinkTemplate']; | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
opts.pluginOptionLikeLink = (plugin, entrypoint, what, name, current_plugin) => | ||
template | ||
.replace(/{plugin_fqcn}/g, plugin.fqcn) | ||
.replace(/{plugin_fqcn_slashes}/g, plugin.fqcn.replace(/\./g, '/')) | ||
.replace(/{plugin_type}/g, plugin.type) | ||
.replace(/{what}/g, what) | ||
.replace(/{entrypoint}/g, entrypoint || '') | ||
.replace(/{entrypoint_with_leading_dash}/g, entrypoint ? '-' + entrypoint : '') | ||
.replace(/{name_dots}/g, name.join('.')) | ||
.replace(/{name_slashes}/g, name.join('/')); | ||
} | ||
} | ||
describe('vectors', (): void => { | ||
@@ -24,16 +49,6 @@ const data = readFileSync('test-vectors.yaml', 'utf8'); | ||
if (test_data.html_opts) { | ||
if (test_data.html_opts['pluginLink.js']) { | ||
test_data.html_opts.pluginLink = eval(test_data.html_opts['pluginLink.js']); | ||
} | ||
if (test_data.html_opts['pluginOptionLikeLink.js']) { | ||
test_data.html_opts.pluginOptionLikeLink = eval(test_data.html_opts['pluginOptionLikeLink.js']); | ||
} | ||
parseLinkProvider(test_data.html_opts); | ||
} | ||
if (test_data.md_opts) { | ||
if (test_data.md_opts['pluginLink.js']) { | ||
test_data.md_opts.pluginLink = eval(test_data.md_opts['pluginLink.js']); | ||
} | ||
if (test_data.md_opts['pluginOptionLikeLink.js']) { | ||
test_data.md_opts.pluginOptionLikeLink = eval(test_data.md_opts['pluginOptionLikeLink.js']); | ||
} | ||
parseLinkProvider(test_data.md_opts); | ||
} | ||
@@ -40,0 +55,0 @@ if (test_data.source !== undefined && test_data.html !== undefined) { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
469276
104
5084
13