diff --git a/lib/index.ts b/lib/index.ts index a97fa06..8037f5c 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -14,10 +14,19 @@ declare module 'unified' { } } -export default function remarkAttributeList(this: Processor) { +export interface Options { + allowNoSpaceBeforeName?: boolean; + allowUnderscoreInId?: boolean; +} + +export default function remarkAttributeList( + this: Processor, + options?: Options, +) { const data = this.data(); + data.micromarkExtensions ??= []; - data.micromarkExtensions.push(micromarkExtension); + data.micromarkExtensions.push(micromarkExtension(options)); data.fromMarkdownExtensions ??= []; data.fromMarkdownExtensions.push(fromMarkdownExtension); diff --git a/lib/micromark/block-inline.ts b/lib/micromark/block-inline.ts index 5712020..1e9c8a9 100644 --- a/lib/micromark/block-inline.ts +++ b/lib/micromark/block-inline.ts @@ -5,6 +5,7 @@ import type { State, TokenizeContext, } from 'micromark-util-types'; +import type {Options} from '../index.js'; import {attributeList} from './list.js'; declare module 'micromark-util-types' { @@ -24,45 +25,48 @@ declare module 'micromark-util-types' { } } -export const blockInlineAttributeList: Construct = { - tokenize, -}; - -function tokenize( - this: TokenizeContext, - effects: Effects, - ok: State, - nok: State, -): State { - const start: State = (code) => { - if (code !== codes.leftCurlyBrace) return nok(code); - effects.enter('blockInlineAttributeList'); - effects.enter('blockInlineAttributeListMarker'); - effects.consume(code); - effects.exit('blockInlineAttributeListMarker'); - return colon; +export function blockInlineAttributeList(options?: Options): Construct { + const list = attributeList(options); + return { + tokenize, }; - const colon: State = (code) => { - if (code === codes.colon) { + function tokenize( + this: TokenizeContext, + effects: Effects, + ok: State, + nok: State, + ): State { + const start: State = (code) => { + if (code !== codes.leftCurlyBrace) return nok(code); + effects.enter('blockInlineAttributeList'); effects.enter('blockInlineAttributeListMarker'); effects.consume(code); effects.exit('blockInlineAttributeListMarker'); + return colon; + }; - return effects.attempt(attributeList, end, nok); - } + const colon: State = (code) => { + if (code === codes.colon) { + effects.enter('blockInlineAttributeListMarker'); + effects.consume(code); + effects.exit('blockInlineAttributeListMarker'); - return nok(code); - }; + return effects.attempt(list, end, nok); + } - const end: State = (code) => { - if (code !== codes.rightCurlyBrace) return nok(code); - effects.enter('blockInlineAttributeListMarker'); - effects.consume(code); - effects.exit('blockInlineAttributeListMarker'); - effects.exit('blockInlineAttributeList'); - return ok; - }; + return nok(code); + }; + + const end: State = (code) => { + if (code !== codes.rightCurlyBrace) return nok(code); + effects.enter('blockInlineAttributeListMarker'); + effects.consume(code); + effects.exit('blockInlineAttributeListMarker'); + effects.exit('blockInlineAttributeList'); + return ok; + }; - return start; + return start; + } } diff --git a/lib/micromark/definition.ts b/lib/micromark/definition.ts index e3ff7ef..49f9e5c 100644 --- a/lib/micromark/definition.ts +++ b/lib/micromark/definition.ts @@ -7,6 +7,7 @@ import type { } from 'micromark-util-types'; import {codes} from 'micromark-util-symbol'; import {ok as assert} from 'devlop'; +import type {Options} from '../index.js'; import {referenceNameIsh} from './reference-name-ish.js'; import {attributeList} from './list.js'; @@ -40,71 +41,74 @@ declare module 'micromark-util-types' { } } -export const attributeListDefinition: Construct = { - tokenize, - resolve, -}; - -function tokenize( - this: TokenizeContext, - effects: Effects, - ok: State, - nok: State, -): State { - const start: State = (code) => { - if (code !== codes.leftCurlyBrace) return nok(code); - effects.enter('attributeListDefinition'); - effects.enter('attributeListDefinitionMarker'); - effects.consume(code); - effects.exit('attributeListDefinitionMarker'); - return referenceStart; +export function attributeListDefinition(options?: Options): Construct { + const list = attributeList(options); + return { + tokenize, + resolve, }; - const referenceStart: State = (code) => { - if (code === codes.colon) { - effects.enter('attributeListDefinitionReference'); - effects.enter('attributeListDefinitionReferenceMarker'); + function tokenize( + this: TokenizeContext, + effects: Effects, + ok: State, + nok: State, + ): State { + const start: State = (code) => { + if (code !== codes.leftCurlyBrace) return nok(code); + effects.enter('attributeListDefinition'); + effects.enter('attributeListDefinitionMarker'); effects.consume(code); - effects.exit('attributeListDefinitionReferenceMarker'); - return effects.attempt(referenceNameIsh, referenceEnd, nok); - } + effects.exit('attributeListDefinitionMarker'); + return referenceStart; + }; - return nok(code); - }; + const referenceStart: State = (code) => { + if (code === codes.colon) { + effects.enter('attributeListDefinitionReference'); + effects.enter('attributeListDefinitionReferenceMarker'); + effects.consume(code); + effects.exit('attributeListDefinitionReferenceMarker'); + return effects.attempt(referenceNameIsh, referenceEnd, nok); + } - const referenceEnd: State = (code) => { - if (code !== codes.colon) return nok(code); - effects.enter('attributeListDefinitionReferenceMarker'); - effects.consume(code); - effects.exit('attributeListDefinitionReferenceMarker'); - effects.exit('attributeListDefinitionReference'); + return nok(code); + }; - return effects.attempt(attributeList, end, nok); - }; + const referenceEnd: State = (code) => { + if (code !== codes.colon) return nok(code); + effects.enter('attributeListDefinitionReferenceMarker'); + effects.consume(code); + effects.exit('attributeListDefinitionReferenceMarker'); + effects.exit('attributeListDefinitionReference'); - const end: State = (code) => { - if (code !== codes.rightCurlyBrace) return nok(code); - effects.enter('attributeListDefinitionMarker'); - effects.consume(code); - effects.exit('attributeListDefinitionMarker'); - effects.exit('attributeListDefinition'); - return ok; - }; + return effects.attempt(list, end, nok); + }; - return start; -} + const end: State = (code) => { + if (code !== codes.rightCurlyBrace) return nok(code); + effects.enter('attributeListDefinitionMarker'); + effects.consume(code); + effects.exit('attributeListDefinitionMarker'); + effects.exit('attributeListDefinition'); + return ok; + }; -function resolve(events: Event[]): Event[] { - const exitReferenceMarkerStart = events.findIndex( - ([type, token]) => - type === 'exit' && - token.type === 'attributeListDefinitionReferenceMarker', - ); + return start; + } + + function resolve(events: Event[]): Event[] { + const exitReferenceMarkerStart = events.findIndex( + ([type, token]) => + type === 'exit' && + token.type === 'attributeListDefinitionReferenceMarker', + ); - const enterReferenceNameIsh = events[exitReferenceMarkerStart + 1]; - assert(enterReferenceNameIsh?.[0] === 'enter'); - assert(enterReferenceNameIsh?.[1].type === 'referenceNameIsh'); - enterReferenceNameIsh[1].type = 'attributeListDefinitionReferenceName'; + const enterReferenceNameIsh = events[exitReferenceMarkerStart + 1]; + assert(enterReferenceNameIsh?.[0] === 'enter'); + assert(enterReferenceNameIsh?.[1].type === 'referenceNameIsh'); + enterReferenceNameIsh[1].type = 'attributeListDefinitionReferenceName'; - return events; + return events; + } } diff --git a/lib/micromark/index.ts b/lib/micromark/index.ts index 24ee441..d32bc9f 100644 --- a/lib/micromark/index.ts +++ b/lib/micromark/index.ts @@ -1,17 +1,20 @@ import type {Extension as MicromarkExtension} from 'micromark-util-types'; import {codes} from 'micromark-util-symbol'; +import type {Options} from '../index.js'; import {attributeListDefinition} from './definition.js'; import {blockInlineAttributeList} from './block-inline.js'; import {spanInlineAttributeList} from './span-inline.js'; -export const micromarkExtension: MicromarkExtension = { - contentInitial: { - [codes.leftCurlyBrace]: attributeListDefinition, - }, - flow: { - [codes.leftCurlyBrace]: blockInlineAttributeList, - }, - text: { - [codes.leftCurlyBrace]: spanInlineAttributeList, - }, -}; +export function micromarkExtension(options?: Options): MicromarkExtension { + return { + contentInitial: { + [codes.leftCurlyBrace]: attributeListDefinition(options), + }, + flow: { + [codes.leftCurlyBrace]: blockInlineAttributeList(options), + }, + text: { + [codes.leftCurlyBrace]: spanInlineAttributeList(options), + }, + }; +} diff --git a/lib/micromark/list.ts b/lib/micromark/list.ts index a925130..c269689 100644 --- a/lib/micromark/list.ts +++ b/lib/micromark/list.ts @@ -14,6 +14,7 @@ import type { TokenizeContext, } from 'micromark-util-types'; import {ok as assert} from 'devlop'; +import type {Options} from '../index.js'; import {referenceNameIsh} from './reference-name-ish.js'; declare module 'micromark-util-types' { @@ -93,240 +94,264 @@ declare module 'micromark-util-types' { } } -export const attributeList: Construct = { - tokenize, - resolve, -}; - -function tokenize( - this: TokenizeContext, - effects: Effects, - ok: State, - nok: State, -): State { - const start: State = (code) => { - effects.enter('attributeList'); - return next(code); +export function attributeList(options?: Options): Construct { + return { + tokenize, + resolve, }; - const next: State = (code) => { - if (unicodeWhitespace(code)) { - return spaceOrEnd(code); - } - - if (code === codes.numberSign) { - effects.enter('idNameAttribute'); - effects.enter('idNameAttributeMarker'); - effects.consume(code); - effects.exit('idNameAttributeMarker'); - return idNameAttributeNameFirst; - } - - if (code === codes.dot) { - effects.enter('classNameAttribute'); - effects.enter('classNameAttributeMarker'); - effects.consume(code); - effects.exit('classNameAttributeMarker'); - effects.enter('classNameAttributeName'); - return classNameAttributeName; - } - - if (asciiAlphanumeric(code)) { - return effects.attempt( - referenceNameIsh, - referenceEndOrKeyValueEqual, - nok, - )(code); - } - - return nok(code); - }; - - let spaces = 0; - const spaceOrEnd: State = (code) => { - if (code === codes.rightCurlyBrace) { - if (spaces > 0) effects.exit('attributeListSpace'); - effects.exit('attributeList'); - return ok(code); - } - - if (unicodeWhitespace(code)) { - if (spaces === 0) effects.enter('attributeListSpace'); - spaces++; - effects.consume(code); - return spaceOrEnd; - } - - if (spaces > 0) { - effects.exit('attributeListSpace'); - spaces = 0; + function tokenize( + this: TokenizeContext, + effects: Effects, + ok: State, + nok: State, + ): State { + const start: State = (code) => { + effects.enter('attributeList'); return next(code); - } + }; + + const next: State = (code) => { + if (unicodeWhitespace(code)) { + return spaceOrEnd(code); + } + + if (code === codes.numberSign) { + effects.enter('idNameAttribute'); + effects.enter('idNameAttributeMarker'); + effects.consume(code); + effects.exit('idNameAttributeMarker'); + return idNameAttributeNameFirst; + } + + if (code === codes.dot) { + effects.enter('classNameAttribute'); + effects.enter('classNameAttributeMarker'); + effects.consume(code); + effects.exit('classNameAttributeMarker'); + effects.enter('classNameAttributeName'); + return classNameAttributeName; + } + + if (asciiAlphanumeric(code)) { + return effects.attempt( + referenceNameIsh, + referenceEndOrKeyValueEqual, + nok, + )(code); + } - return nok(code); - }; + return nok(code); + }; + + let spaces = 0; + const spaceOrEnd: State = (code) => { + if (code === codes.rightCurlyBrace) { + if (spaces > 0) effects.exit('attributeListSpace'); + effects.exit('attributeList'); + return ok(code); + } + + if (unicodeWhitespace(code)) { + if (spaces === 0) effects.enter('attributeListSpace'); + spaces++; + effects.consume(code); + return spaceOrEnd; + } + + if (spaces > 0) { + effects.exit('attributeListSpace'); + spaces = 0; + return next(code); + } - const idNameAttributeNameFirst: State = (code) => { - if (!asciiAlpha(code)) return nok(code); - effects.enter('idNameAttributeName'); - effects.consume(code); - return idNameAttributeNameRest; - }; + return nok(code); + }; - const idNameAttributeNameRest: State = (code) => { - if ( - asciiAlphanumeric(code) || - code === codes.dash || - code === codes.colon - ) { + const idNameAttributeNameFirst: State = (code) => { + if (!asciiAlpha(code)) return nok(code); + effects.enter('idNameAttributeName'); effects.consume(code); return idNameAttributeNameRest; - } + }; + + const idNameAttributeNameRest: State = (code) => { + if ( + asciiAlphanumeric(code) || + code === codes.dash || + code === codes.colon || + (options?.allowUnderscoreInId && code === codes.underscore) + ) { + effects.consume(code); + return idNameAttributeNameRest; + } + + effects.exit('idNameAttributeName'); + effects.exit('idNameAttribute'); + + if ( + options?.allowNoSpaceBeforeName && + (code === codes.dot || code === codes.numberSign) + ) { + return next(code); + } - effects.exit('idNameAttributeName'); - effects.exit('idNameAttribute'); - return spaceOrEnd(code); - }; - - const classNameAttributeName: State = (code) => { - if (unicodeWhitespace(code) || code === codes.rightCurlyBrace) { - effects.exit('classNameAttributeName'); - effects.exit('classNameAttribute'); return spaceOrEnd(code); - } + }; - if (code === codes.dot || code === codes.numberSign) { - return nok(code); - } + const classNameAttributeName: State = (code) => { + if (unicodeWhitespace(code) || code === codes.rightCurlyBrace) { + effects.exit('classNameAttributeName'); + effects.exit('classNameAttribute'); + return spaceOrEnd(code); + } - effects.consume(code); - return classNameAttributeName; - }; + if (code === codes.dot || code === codes.numberSign) { + if (options?.allowNoSpaceBeforeName) { + effects.exit('classNameAttributeName'); + effects.exit('classNameAttribute'); + return next(code); + } - const referenceEndOrKeyValueEqual: State = (code) => { - if (code === codes.equalsTo) { - effects.enter('keyValuePairAttributeEquals'); - effects.consume(code); - effects.exit('keyValuePairAttributeEquals'); - return keyValuePairAttributeValueStart; - } - - // `effects.exit('referenceAttribute')` will be added later on resolveAll - return spaceOrEnd(code); - }; + return nok(code); + } - let keyValuePairAttributeValueMarker: Code | undefined; - const keyValuePairAttributeValueStart: State = (code) => { - if (code === codes.quotationMark || code === codes.apostrophe) { - effects.enter('keyValuePairAttributeValue'); - effects.enter('keyValuePairAttributeValueMarker'); - keyValuePairAttributeValueMarker = code; effects.consume(code); - effects.exit('keyValuePairAttributeValueMarker'); - effects.enter('keyValuePairAttributeValueString'); - return keyValuePairAttributeValueString; - } - - return nok(code); - }; + return classNameAttributeName; + }; + + const referenceEndOrKeyValueEqual: State = (code) => { + if (code === codes.equalsTo) { + effects.enter('keyValuePairAttributeEquals'); + effects.consume(code); + effects.exit('keyValuePairAttributeEquals'); + return keyValuePairAttributeValueStart; + } + + if ( + options?.allowNoSpaceBeforeName && + (code === codes.dot || code === codes.numberSign) + ) { + return next(code); + } + + // `effects.exit('referenceAttribute')` will be added later on resolveAll + return spaceOrEnd(code); + }; + + let keyValuePairAttributeValueMarker: Code | undefined; + const keyValuePairAttributeValueStart: State = (code) => { + if (code === codes.quotationMark || code === codes.apostrophe) { + effects.enter('keyValuePairAttributeValue'); + effects.enter('keyValuePairAttributeValueMarker'); + keyValuePairAttributeValueMarker = code; + effects.consume(code); + effects.exit('keyValuePairAttributeValueMarker'); + effects.enter('keyValuePairAttributeValueString'); + return keyValuePairAttributeValueString; + } - let escaping = false; - const keyValuePairAttributeValueString: State = (code) => { - if (escaping) { - effects.consume(code); - escaping = false; - return keyValuePairAttributeValueString; - } + return nok(code); + }; + + let escaping = false; + const keyValuePairAttributeValueString: State = (code) => { + if (escaping) { + effects.consume(code); + escaping = false; + return keyValuePairAttributeValueString; + } + + if (code === codes.backslash) { + effects.consume(code); + escaping = true; + return keyValuePairAttributeValueString; + } + + if (code === keyValuePairAttributeValueMarker) { + effects.exit('keyValuePairAttributeValueString'); + effects.enter('keyValuePairAttributeValueMarker'); + effects.consume(code); + keyValuePairAttributeValueMarker = undefined; + effects.exit('keyValuePairAttributeValueMarker'); + effects.exit('keyValuePairAttributeValue'); + // `effects.exit('keyValuePairAttribute')` will be added later on resolveAll + return spaceOrEnd; + } - if (code === codes.backslash) { effects.consume(code); - escaping = true; return keyValuePairAttributeValueString; - } - - if (code === keyValuePairAttributeValueMarker) { - effects.exit('keyValuePairAttributeValueString'); - effects.enter('keyValuePairAttributeValueMarker'); - effects.consume(code); - keyValuePairAttributeValueMarker = undefined; - effects.exit('keyValuePairAttributeValueMarker'); - effects.exit('keyValuePairAttributeValue'); - // `effects.exit('keyValuePairAttribute')` will be added later on resolveAll - return spaceOrEnd; - } + }; - effects.consume(code); - return keyValuePairAttributeValueString; - }; - - return start; -} + return start; + } -function resolve(events: Event[], context: TokenizeContext): Event[] { - let index = 0; - while (index < events.length) { - const [enter, referenceNameIsh] = events[index]!; - if (enter !== 'enter' || referenceNameIsh.type !== 'referenceNameIsh') { - index++; - continue; + function resolve(events: Event[], context: TokenizeContext): Event[] { + let index = 0; + while (index < events.length) { + const [enter, referenceNameIsh] = events[index]!; + if (enter !== 'enter' || referenceNameIsh.type !== 'referenceNameIsh') { + index++; + continue; + } + + if (events[index + 2]?.[1].type === 'keyValuePairAttributeEquals') { + // `referenceNameIsh` is a keyValuePairAttributeKey + // Expected condition: + // - enter keyValuePairAttribute <- added + // - enter keyValuePairAttributeKey <- index, renamed + // - exit keyValuePairAttributeKey <- renamed + // - enter keyValuePairAttributeEquals + // - exit keyValuePairAttributeEquals + // - enter keyValuePairAttributeValue + // - enter keyValuePairAttributeValueMarker + // - exit keyValuePairAttributeValueMarker + // - enter keyValuePairAttributeValueString + // - exit keyValuePairAttributeValueString + // - enter keyValuePairAttributeValueMarker + // - exit keyValuePairAttributeValueMarker + // - exit keyValuePairAttributeValue + // - exit keyValuePairAttribute <- added + + referenceNameIsh.type = 'keyValuePairAttributeKey'; + + const exitKeyValuePairAttribute = events[index + 11]; + assert(exitKeyValuePairAttribute); + assert(exitKeyValuePairAttribute[0] === 'exit'); + assert( + exitKeyValuePairAttribute[1].type === 'keyValuePairAttributeValue', + `Expected keyValuePairAttributeValue, got ${exitKeyValuePairAttribute[1].type}`, + ); + const [, keyValuePairAttributeValue] = exitKeyValuePairAttribute; + const keyValuePairAttribute: Token = { + type: 'keyValuePairAttribute', + start: referenceNameIsh.start, + end: keyValuePairAttributeValue.end, + }; + events.splice(index, 0, ['enter', keyValuePairAttribute, context]); + events.splice(index + 13, 0, ['exit', keyValuePairAttribute, context]); + index += 14; + } else { + // `referenceNameIsh` is a referenceAttributeName + // Expected condition: + // - enter referenceAttribute <- added + // - enter referenceAttributeName <- index, renamed + // - exit referenceAttributeName <- renamed + // - exit referenceAttribute <- added + + referenceNameIsh.type = 'referenceAttributeName'; + const referenceAttribute: Token = { + type: 'referenceAttribute', + start: referenceNameIsh.start, + end: referenceNameIsh.end, + }; + events.splice(index, 0, ['enter', referenceAttribute, context]); + events.splice(index + 3, 0, ['exit', referenceAttribute, context]); + + index += 4; + } } - if (events[index + 2]?.[1].type === 'keyValuePairAttributeEquals') { - // `referenceNameIsh` is a keyValuePairAttributeKey - // Expected condition: - // - enter keyValuePairAttribute <- added - // - enter keyValuePairAttributeKey <- index, renamed - // - exit keyValuePairAttributeKey <- renamed - // - enter keyValuePairAttributeEquals - // - exit keyValuePairAttributeEquals - // - enter keyValuePairAttributeValue - // - enter keyValuePairAttributeValueMarker - // - exit keyValuePairAttributeValueMarker - // - enter keyValuePairAttributeValueString - // - exit keyValuePairAttributeValueString - // - enter keyValuePairAttributeValueMarker - // - exit keyValuePairAttributeValueMarker - // - exit keyValuePairAttributeValue - // - exit keyValuePairAttribute <- added - - referenceNameIsh.type = 'keyValuePairAttributeKey'; - - const exitKeyValuePairAttribute = events[index + 11]; - assert(exitKeyValuePairAttribute); - assert(exitKeyValuePairAttribute[0] === 'exit'); - assert( - exitKeyValuePairAttribute[1].type === 'keyValuePairAttributeValue', - `Expected keyValuePairAttributeValue, got ${exitKeyValuePairAttribute[1].type}`, - ); - const [, keyValuePairAttributeValue] = exitKeyValuePairAttribute; - const keyValuePairAttribute: Token = { - type: 'keyValuePairAttribute', - start: referenceNameIsh.start, - end: keyValuePairAttributeValue.end, - }; - events.splice(index, 0, ['enter', keyValuePairAttribute, context]); - events.splice(index + 13, 0, ['exit', keyValuePairAttribute, context]); - index += 14; - } else { - // `referenceNameIsh` is a referenceAttributeName - // Expected condition: - // - enter referenceAttribute <- added - // - enter referenceAttributeName <- index, renamed - // - exit referenceAttributeName <- renamed - // - exit referenceAttribute <- added - - referenceNameIsh.type = 'referenceAttributeName'; - const referenceAttribute: Token = { - type: 'referenceAttribute', - start: referenceNameIsh.start, - end: referenceNameIsh.end, - }; - events.splice(index, 0, ['enter', referenceAttribute, context]); - events.splice(index + 3, 0, ['exit', referenceAttribute, context]); - - index += 4; - } + return events; } - - return events; } diff --git a/lib/micromark/span-inline.ts b/lib/micromark/span-inline.ts index 5034f2f..f79c9a8 100644 --- a/lib/micromark/span-inline.ts +++ b/lib/micromark/span-inline.ts @@ -5,6 +5,7 @@ import type { State, TokenizeContext, } from 'micromark-util-types'; +import type {Options} from '../index.js'; import {attributeList} from './list.js'; declare module 'micromark-util-types' { @@ -24,57 +25,60 @@ declare module 'micromark-util-types' { } } -export const spanInlineAttributeList: Construct = { - tokenize, -}; - -function tokenize( - this: TokenizeContext, - effects: Effects, - ok: State, - nok: State, -): State { - const start: State = (code) => { - if (code !== codes.leftCurlyBrace) return nok(code); - effects.enter('spanInlineAttributeList'); - effects.enter('spanInlineAttributeListMarker'); - effects.consume(code); - effects.exit('spanInlineAttributeListMarker'); - return colon; +export function spanInlineAttributeList(options?: Options): Construct { + const list = attributeList(options); + return { + tokenize, }; - const colon: State = (code) => { - if (code === codes.colon) { + function tokenize( + this: TokenizeContext, + effects: Effects, + ok: State, + nok: State, + ): State { + const start: State = (code) => { + if (code !== codes.leftCurlyBrace) return nok(code); + effects.enter('spanInlineAttributeList'); effects.enter('spanInlineAttributeListMarker'); effects.consume(code); effects.exit('spanInlineAttributeListMarker'); + return colon; + }; - return listOrColon; - } + const colon: State = (code) => { + if (code === codes.colon) { + effects.enter('spanInlineAttributeListMarker'); + effects.consume(code); + effects.exit('spanInlineAttributeListMarker'); - return nok(code); - }; + return listOrColon; + } - const listOrColon: State = (code) => { - if (code === codes.colon) { - effects.enter('spanInlineAttributeListMarker'); - effects.consume(code); - effects.exit('spanInlineAttributeListMarker'); + return nok(code); + }; - return end; - } + const listOrColon: State = (code) => { + if (code === codes.colon) { + effects.enter('spanInlineAttributeListMarker'); + effects.consume(code); + effects.exit('spanInlineAttributeListMarker'); - return effects.attempt(attributeList, end, nok)(code); - }; + return end; + } - const end: State = (code) => { - if (code !== codes.rightCurlyBrace) return nok(code); - effects.enter('spanInlineAttributeListMarker'); - effects.consume(code); - effects.exit('spanInlineAttributeListMarker'); - effects.exit('spanInlineAttributeList'); - return ok; - }; + return effects.attempt(list, end, nok)(code); + }; + + const end: State = (code) => { + if (code !== codes.rightCurlyBrace) return nok(code); + effects.enter('spanInlineAttributeListMarker'); + effects.consume(code); + effects.exit('spanInlineAttributeListMarker'); + effects.exit('spanInlineAttributeList'); + return ok; + }; - return start; + return start; + } } diff --git a/test/options.js b/test/options.js new file mode 100644 index 0000000..e1a6595 --- /dev/null +++ b/test/options.js @@ -0,0 +1,70 @@ +import {test} from 'node:test'; +import assert from 'node:assert'; +import {unified} from 'unified'; +import remarkParse from 'remark-parse'; +import {removePosition} from 'unist-util-remove-position'; +import {u} from 'unist-builder'; +import remarkAttributeList from '../dist/index.js'; + +const processor = unified().use(remarkParse).use(remarkAttributeList, { + allowNoSpaceBeforeName: true, + allowUnderscoreInId: true, +}); + +/** + * @param {string} source + */ +function parse(source) { + const parsed = processor.parse(source); + removePosition(parsed, {force: true}); + return parsed; +} + +await test('parsing with options', async (t) => { + await t.test( + 'should parse attribute list without space before name', + async () => { + assert.deepStrictEqual( + parse('{:name:.cls1.cls2}\n'), + u('root', [ + u('attributeListDefinition', {name: 'name'}, [ + u('classNameAttribute', {name: 'cls1'}), + u('classNameAttribute', {name: 'cls2'}), + ]), + ]), + ); + assert.deepStrictEqual( + parse('{:#id.cls1}\n'), + u('root', [ + u('blockInlineAttributeList', [ + u('idNameAttribute', {name: 'id'}), + u('classNameAttribute', {name: 'cls1'}), + ]), + ]), + ); + assert.deepStrictEqual( + parse('{:ref#id}\n'), + u('root', [ + u('blockInlineAttributeList', [ + u('referenceAttribute', {name: 'ref'}), + u('idNameAttribute', {name: 'id'}), + ]), + ]), + ); + }, + ); + + await t.test( + 'should parse attribute list with underscore in ID', + async () => { + assert.deepStrictEqual( + parse('{:ref:#id_name}\n'), + u('root', [ + u('attributeListDefinition', {name: 'ref'}, [ + u('idNameAttribute', {name: 'id_name'}), + ]), + ]), + ); + }, + ); +});