diff --git a/changelogs/fragments/37-source.yml b/changelogs/fragments/37-source.yml new file mode 100644 index 0000000..4580d43 --- /dev/null +++ b/changelogs/fragments/37-source.yml @@ -0,0 +1,4 @@ +minor_changes: + - "Allow to add markup source to every paragraph part (https://github.com/ansible-community/antsibull-docs-ts/pull/37)." +breaking_changes: + - "All DOM parts have a new ``source`` property, which must be a string or ``undefined`` (https://github.com/ansible-community/antsibull-docs-ts/pull/37)." diff --git a/src/dom.ts b/src/dom.ts index f2dbbd2..5879333 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -26,6 +26,7 @@ export enum PartType { export interface Part { type: PartType; + source: string | undefined; } export interface TextPart extends Part { diff --git a/src/opts.ts b/src/opts.ts index d1f84a7..fa29c8e 100644 --- a/src/opts.ts +++ b/src/opts.ts @@ -48,6 +48,9 @@ export interface ParsingOptions extends ErrorHandlingOptions { /** If set to 'true', only 'classic' Ansible docs markup is accepted. */ onlyClassicMarkup?: boolean; + + /** If set to 'true', add source information to every part ('source' attribute). */ + addSource?: boolean; } export interface CommonExportOptions extends ErrorHandlingOptions { diff --git a/src/parser.spec.ts b/src/parser.spec.ts index e964478..d707ae0 100644 --- a/src/parser.spec.ts +++ b/src/parser.spec.ts @@ -15,7 +15,10 @@ describe('parser', (): void => { expect(parse([''])).toEqual([[]]); }); it('simple string', (): void => { - expect(parse('test')).toEqual([[{ type: PartType.TEXT, text: 'test' }]]); + expect(parse('test')).toEqual([[{ type: PartType.TEXT, text: 'test', source: undefined }]]); + }); + it('simple string (with source)', (): void => { + expect(parse('test', { addSource: true })).toEqual([[{ type: PartType.TEXT, text: 'test', source: 'test' }]]); }); it('classic markup test', (): void => { expect( @@ -24,22 +27,49 @@ describe('parser', (): void => { ), ).toEqual([ [ - { type: PartType.TEXT, text: 'foo ' }, - { type: PartType.ITALIC, text: 'bar' }, - { type: PartType.TEXT, text: ' baz ' }, - { type: PartType.CODE, text: ' bam ' }, - { type: PartType.TEXT, text: ' ' }, - { type: PartType.BOLD, text: ' ( boo ' }, - { type: PartType.TEXT, text: ' ) ' }, - { type: PartType.URL, url: 'https://example.com/?foo=bar' }, - { type: PartType.HORIZONTAL_LINE }, - { type: PartType.TEXT, text: ' ' }, - { type: PartType.LINK, text: 'foo', url: 'https://bar.com' }, - { type: PartType.TEXT, text: ' ' }, - { type: PartType.RST_REF, text: ' a', ref: 'b ' }, - { type: PartType.MODULE, fqcn: 'foo.bar.baz' }, - { type: PartType.TEXT, text: 'HORIZONTALLINEx ' }, - { type: PartType.MODULE, fqcn: 'foo.bar.baz.bam' }, + { type: PartType.TEXT, text: 'foo ', source: undefined }, + { type: PartType.ITALIC, text: 'bar', source: undefined }, + { type: PartType.TEXT, text: ' baz ', source: undefined }, + { type: PartType.CODE, text: ' bam ', source: undefined }, + { type: PartType.TEXT, text: ' ', source: undefined }, + { type: PartType.BOLD, text: ' ( boo ', source: undefined }, + { type: PartType.TEXT, text: ' ) ', source: undefined }, + { type: PartType.URL, url: 'https://example.com/?foo=bar', source: undefined }, + { type: PartType.HORIZONTAL_LINE, source: undefined }, + { type: PartType.TEXT, text: ' ', source: undefined }, + { type: PartType.LINK, text: 'foo', url: 'https://bar.com', source: undefined }, + { type: PartType.TEXT, text: ' ', source: undefined }, + { type: PartType.RST_REF, text: ' a', ref: 'b ', source: undefined }, + { type: PartType.MODULE, fqcn: 'foo.bar.baz', source: undefined }, + { type: PartType.TEXT, text: 'HORIZONTALLINEx ', source: undefined }, + { type: PartType.MODULE, fqcn: 'foo.bar.baz.bam', source: undefined }, + ], + ]); + }); + it('classic markup test (with source)', (): void => { + expect( + parse( + 'foo I(bar) baz C( bam ) B( ( boo ) ) U(https://example.com/?foo=bar)HORIZONTALLINE L(foo , https://bar.com) R( a , b )M(foo.bar.baz)HORIZONTALLINEx M(foo.bar.baz.bam)', + { addSource: true }, + ), + ).toEqual([ + [ + { type: PartType.TEXT, text: 'foo ', source: 'foo ' }, + { type: PartType.ITALIC, text: 'bar', source: 'I(bar)' }, + { type: PartType.TEXT, text: ' baz ', source: ' baz ' }, + { type: PartType.CODE, text: ' bam ', source: 'C( bam )' }, + { type: PartType.TEXT, text: ' ', source: ' ' }, + { type: PartType.BOLD, text: ' ( boo ', source: 'B( ( boo )' }, + { type: PartType.TEXT, text: ' ) ', source: ' ) ' }, + { type: PartType.URL, url: 'https://example.com/?foo=bar', source: 'U(https://example.com/?foo=bar)' }, + { 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)' }, + { type: PartType.TEXT, text: ' ', source: ' ' }, + { type: PartType.RST_REF, text: ' a', ref: 'b ', source: 'R( a , b )' }, + { type: PartType.MODULE, fqcn: 'foo.bar.baz', source: 'M(foo.bar.baz)' }, + { type: PartType.TEXT, text: 'HORIZONTALLINEx ', source: 'HORIZONTALLINEx ' }, + { type: PartType.MODULE, fqcn: 'foo.bar.baz.bam', source: 'M(foo.bar.baz.bam)' }, ], ]); }); @@ -51,35 +81,35 @@ describe('parser', (): void => { ), ).toEqual([ [ - { type: PartType.TEXT, text: 'foo ' }, - { type: PartType.ITALIC, text: 'bar' }, - { type: PartType.TEXT, text: ' baz ' }, - { type: PartType.CODE, text: ' bam ' }, - { type: PartType.TEXT, text: ' ' }, - { type: PartType.BOLD, text: ' ( boo ' }, - { type: PartType.TEXT, text: ' ) ' }, - { type: PartType.URL, url: 'https://example.com/?foo=bar' }, - { type: PartType.HORIZONTAL_LINE }, - { type: PartType.TEXT, text: ' ' }, - { type: PartType.LINK, text: 'foo', url: 'https://bar.com' }, - { type: PartType.TEXT, text: ' ' }, - { type: PartType.RST_REF, text: ' a', ref: 'b ' }, - { type: PartType.MODULE, fqcn: 'foo.bar.baz' }, - { type: PartType.TEXT, text: 'HORIZONTALLINEx ' }, - { type: PartType.MODULE, fqcn: 'foo.bar.baz.bam' }, + { type: PartType.TEXT, text: 'foo ', source: undefined }, + { type: PartType.ITALIC, text: 'bar', source: undefined }, + { type: PartType.TEXT, text: ' baz ', source: undefined }, + { type: PartType.CODE, text: ' bam ', source: undefined }, + { type: PartType.TEXT, text: ' ', source: undefined }, + { type: PartType.BOLD, text: ' ( boo ', source: undefined }, + { type: PartType.TEXT, text: ' ) ', source: undefined }, + { type: PartType.URL, url: 'https://example.com/?foo=bar', source: undefined }, + { type: PartType.HORIZONTAL_LINE, source: undefined }, + { type: PartType.TEXT, text: ' ', source: undefined }, + { type: PartType.LINK, text: 'foo', url: 'https://bar.com', source: undefined }, + { type: PartType.TEXT, text: ' ', source: undefined }, + { type: PartType.RST_REF, text: ' a', ref: 'b ', source: undefined }, + { type: PartType.MODULE, fqcn: 'foo.bar.baz', source: undefined }, + { type: PartType.TEXT, text: 'HORIZONTALLINEx ', source: undefined }, + { type: PartType.MODULE, fqcn: 'foo.bar.baz.bam', source: undefined }, ], ]); }); it('semantic markup test', (): void => { expect(parse('foo E(a\\),b) P(foo.bar.baz#bam) baz V( b\\,\\na\\)\\\\m\\, ) O(foo) ')).toEqual([ [ - { type: PartType.TEXT, text: 'foo ' }, - { type: PartType.ENV_VARIABLE, name: 'a),b' }, - { type: PartType.TEXT, text: ' ' }, - { type: PartType.PLUGIN, plugin: { fqcn: 'foo.bar.baz', type: 'bam' } }, - { type: PartType.TEXT, text: ' baz ' }, - { type: PartType.OPTION_VALUE, value: ' b,na)\\m, ' }, - { type: PartType.TEXT, text: ' ' }, + { type: PartType.TEXT, text: 'foo ', source: undefined }, + { type: PartType.ENV_VARIABLE, name: 'a),b', source: undefined }, + { type: PartType.TEXT, text: ' ', source: undefined }, + { type: PartType.PLUGIN, plugin: { fqcn: 'foo.bar.baz', type: 'bam' }, source: undefined }, + { type: PartType.TEXT, text: ' baz ', source: undefined }, + { type: PartType.OPTION_VALUE, value: ' b,na)\\m, ', source: undefined }, + { type: PartType.TEXT, text: ' ', source: undefined }, { type: PartType.OPTION_NAME, plugin: undefined, @@ -87,11 +117,37 @@ describe('parser', (): void => { link: ['foo'], name: 'foo', value: undefined, + source: undefined, }, - { type: PartType.TEXT, text: ' ' }, + { type: PartType.TEXT, text: ' ', source: undefined }, ], ]); }); + it('semantic markup test (with source)', (): void => { + expect(parse('foo E(a\\),b) P(foo.bar.baz#bam) baz V( b\\,\\na\\)\\\\m\\, ) O(foo) ', { addSource: true })).toEqual( + [ + [ + { type: PartType.TEXT, text: 'foo ', source: 'foo ' }, + { type: PartType.ENV_VARIABLE, name: 'a),b', source: 'E(a\\),b)' }, + { type: PartType.TEXT, text: ' ', source: ' ' }, + { type: PartType.PLUGIN, plugin: { fqcn: 'foo.bar.baz', type: 'bam' }, source: 'P(foo.bar.baz#bam)' }, + { type: PartType.TEXT, text: ' baz ', source: ' baz ' }, + { type: PartType.OPTION_VALUE, value: ' b,na)\\m, ', source: 'V( b\\,\\na\\)\\\\m\\, )' }, + { type: PartType.TEXT, text: ' ', source: ' ' }, + { + type: PartType.OPTION_NAME, + plugin: undefined, + entrypoint: undefined, + link: ['foo'], + name: 'foo', + value: undefined, + source: 'O(foo)', + }, + { type: PartType.TEXT, text: ' ', source: ' ' }, + ], + ], + ); + }); it('semantic markup option name', (): void => { expect(parse('O(foo)')).toEqual([ [ @@ -102,6 +158,7 @@ describe('parser', (): void => { link: ['foo'], name: 'foo', value: undefined, + source: undefined, }, ], ]); @@ -114,6 +171,7 @@ describe('parser', (): void => { link: ['foo'], name: 'foo', value: undefined, + source: undefined, }, ], ]); @@ -126,6 +184,7 @@ describe('parser', (): void => { link: ['foo'], name: 'foo', value: undefined, + source: undefined, }, ], ]); @@ -138,6 +197,7 @@ describe('parser', (): void => { link: ['foo'], name: 'foo', value: undefined, + source: undefined, }, ], ]); @@ -150,6 +210,7 @@ describe('parser', (): void => { link: ['foo'], name: 'foo', value: 'bar', + source: undefined, }, ], ]); @@ -162,6 +223,7 @@ describe('parser', (): void => { link: ['foo', 'baz'], name: 'foo.baz', value: 'bam', + source: undefined, }, ], ]); @@ -174,6 +236,7 @@ describe('parser', (): void => { link: ['foo', 'baz', 'boo'], name: 'foo[1].baz[bam.bar.boing].boo', value: undefined, + source: undefined, }, ], ]); @@ -186,6 +249,7 @@ describe('parser', (): void => { link: ['foo', 'baz', 'boo'], name: 'foo[1].baz[bam.bar.boing].boo', value: undefined, + source: undefined, }, ], ]); @@ -200,6 +264,7 @@ describe('parser', (): void => { link: ['foo'], name: 'foo', value: undefined, + source: undefined, }, ], ]); @@ -212,6 +277,7 @@ describe('parser', (): void => { link: ['foo'], name: 'foo', value: undefined, + source: undefined, }, ], ]); @@ -224,6 +290,7 @@ describe('parser', (): void => { link: ['foo'], name: 'foo', value: undefined, + source: undefined, }, ], ]); @@ -236,6 +303,7 @@ describe('parser', (): void => { link: ['foo'], name: 'foo', value: undefined, + source: undefined, }, ], ]); @@ -248,6 +316,7 @@ describe('parser', (): void => { link: ['foo'], name: 'foo', value: 'bar', + source: undefined, }, ], ]); @@ -260,6 +329,7 @@ describe('parser', (): void => { link: ['foo', 'baz'], name: 'foo.baz', value: 'bam', + source: undefined, }, ], ]); @@ -272,6 +342,7 @@ describe('parser', (): void => { link: ['foo', 'baz', 'boo'], name: 'foo[1].baz[bam.bar.boing].boo', value: undefined, + source: undefined, }, ], ]); @@ -284,6 +355,7 @@ describe('parser', (): void => { link: ['foo', 'baz', 'boo'], name: 'foo[1].baz[bam.bar.boing].boo', value: undefined, + source: undefined, }, ], ]); diff --git a/src/parser.ts b/src/parser.ts index ea0bde8..65517d0 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -34,6 +34,7 @@ function parseOptionLike( text: string, opts: ParsingOptions, type: PartType.OPTION_NAME | PartType.RETURN_VALUE, + source: string | undefined, ): OptionNamePart | ReturnValuePart { let value: string | undefined; const eq = text.indexOf('='); @@ -82,6 +83,7 @@ function parseOptionLike( link: text.replace(/\[([^\]]*)\]/g, '').split('.'), name: text, value: value, + source: source, }; } @@ -89,7 +91,7 @@ export interface CommandParser { command: string; parameters: number; escapedArguments?: boolean; - process: (args: string[], opts: ParsingOptions) => AnyPart; + process: (args: string[], opts: ParsingOptions, source: string | undefined) => AnyPart; } interface CommandParserEx extends CommandParser { @@ -102,76 +104,76 @@ const PARSER: CommandParserEx[] = [ command: 'I', parameters: 1, old_markup: true, - process: (args) => { + process: (args, _, source) => { const text = args[0] as string; - return { type: PartType.ITALIC, text: text }; + return { type: PartType.ITALIC, text: text, source: source }; }, }, { command: 'B', parameters: 1, old_markup: true, - process: (args) => { + process: (args, _, source) => { const text = args[0] as string; - return { type: PartType.BOLD, text: text }; + return { type: PartType.BOLD, text: text, source: source }; }, }, { command: 'M', parameters: 1, old_markup: true, - process: (args) => { + process: (args, _, source) => { const fqcn = args[0] as string; if (!isFQCN(fqcn)) { throw Error(`Module name "${fqcn}" is not a FQCN`); } - return { type: PartType.MODULE, fqcn: fqcn }; + return { type: PartType.MODULE, fqcn: fqcn, source: source }; }, }, { command: 'U', parameters: 1, old_markup: true, - process: (args) => { + process: (args, _, source) => { const url = args[0] as string; - return { type: PartType.URL, url: url }; + return { type: PartType.URL, url: url, source: source }; }, }, { command: 'L', parameters: 2, old_markup: true, - process: (args) => { + process: (args, _, source) => { const text = args[0] as string; const url = args[1] as string; - return { type: PartType.LINK, text: text, url: url }; + return { type: PartType.LINK, text: text, url: url, source: source }; }, }, { command: 'R', parameters: 2, old_markup: true, - process: (args) => { + process: (args, _, source) => { const text = args[0] as string; const ref = args[1] as string; - return { type: PartType.RST_REF, text: text, ref: ref }; + return { type: PartType.RST_REF, text: text, ref: ref, source: source }; }, }, { command: 'C', parameters: 1, old_markup: true, - process: (args) => { + process: (args, _, source) => { const text = args[0] as string; - return { type: PartType.CODE, text: text }; + return { type: PartType.CODE, text: text, source: source }; }, }, { command: 'HORIZONTALLINE', parameters: 0, old_markup: true, - process: () => { - return { type: PartType.HORIZONTAL_LINE }; + process: (_, __, source) => { + return { type: PartType.HORIZONTAL_LINE, source: source }; }, }, // Semantic Ansible docs markup: @@ -179,7 +181,7 @@ const PARSER: CommandParserEx[] = [ command: 'P', parameters: 1, escapedArguments: true, - process: (args) => { + process: (args, _, source) => { const m = /^([^#]*)#(.*)$/.exec(args[0] as string); if (!m) { throw Error(`Parameter "${args[0]}" is not of the form FQCN#type`); @@ -192,43 +194,43 @@ const PARSER: CommandParserEx[] = [ if (!isPluginType(type)) { throw Error(`Plugin type "${type}" is not valid`); } - return { type: PartType.PLUGIN, plugin: { fqcn: fqcn, type: type } }; + return { type: PartType.PLUGIN, plugin: { fqcn: fqcn, type: type }, source: source }; }, }, { command: 'E', parameters: 1, escapedArguments: true, - process: (args) => { + process: (args, _, source) => { const env = args[0] as string; - return { type: PartType.ENV_VARIABLE, name: env }; + return { type: PartType.ENV_VARIABLE, name: env, source: source }; }, }, { command: 'V', parameters: 1, escapedArguments: true, - process: (args) => { + process: (args, _, source) => { const value = args[0] as string; - return { type: PartType.OPTION_VALUE, value: value }; + return { type: PartType.OPTION_VALUE, value: value, source: source }; }, }, { command: 'O', parameters: 1, escapedArguments: true, - process: (args, opts) => { + process: (args, opts, source) => { const value = args[0] as string; - return parseOptionLike(value, opts, PartType.OPTION_NAME); + return parseOptionLike(value, opts, PartType.OPTION_NAME, source); }, }, { command: 'RV', parameters: 1, escapedArguments: true, - process: (args, opts) => { + process: (args, opts, source) => { const value = args[0] as string; - return parseOptionLike(value, opts, PartType.RETURN_VALUE); + return parseOptionLike(value, opts, PartType.RETURN_VALUE, source); }, }, ]; @@ -266,17 +268,21 @@ export function parseString( const match = commandRE.exec(input); if (!match) { if (index < length) { + const text = input.slice(index); result.push({ type: PartType.TEXT, - text: input.slice(index), + text: text, + source: opts.addSource ? text : undefined, }); } break; } if (match.index > index) { + const text = input.slice(index, match.index); result.push({ type: PartType.TEXT, - text: input.slice(index, match.index), + text: text, + source: opts.addSource ? text : undefined, }); } index = match.index; @@ -298,9 +304,10 @@ export function parseString( } else { [args, endIndex, error] = parseUnescapedArgs(input, endIndex, command.parameters); } + const source = opts.addSource ? input.slice(index, endIndex) : undefined; if (error === undefined) { try { - result.push(command.process(args, opts)); + result.push(command.process(args, opts, source)); } catch (exc: any) { error = `${exc}`; if (exc?.message !== undefined) { @@ -317,6 +324,7 @@ export function parseString( result.push({ type: PartType.ERROR, message: error, + source: source, }); break; case 'exception':