diff --git a/src/from-markdown.ts b/src/from-markdown.ts index 95b3c40..0e38983 100644 --- a/src/from-markdown.ts +++ b/src/from-markdown.ts @@ -245,7 +245,9 @@ export default (opts: RemarkMDCOptions = {}) => { function exitAttributeValue (this: CompileContext, token: Token) { const attributes = (this.data as any).componentAttributes - attributes[attributes.length - 1][1] = parseEntities(this.sliceSerialize(token)) + const lastAttribute = attributes[attributes.length - 1] + + lastAttribute[1] = (typeof lastAttribute[1] === 'string' ? lastAttribute[1] : '') + parseEntities(this.sliceSerialize(token)) } function exitAttributeName (this: CompileContext, token: Token) { diff --git a/src/micromark-extension/factory-attributes.ts b/src/micromark-extension/factory-attributes.ts index 74cd6e7..0e338fa 100644 --- a/src/micromark-extension/factory-attributes.ts +++ b/src/micromark-extension/factory-attributes.ts @@ -296,6 +296,17 @@ export default function createAttributes ( } function valueQuoted (code: number) { + if (code === Codes.backSlash) { + effects.exit(attributeValueData) + effects.exit(attributeValueType) + effects.enter('escapeCharacter') + effects.consume(code) + effects.exit('escapeCharacter') + effects.enter(attributeValueType) + effects.enter(attributeValueData) + return valueQuotedEscape + } + if (code === marker || code === Codes.EOF || markdownLineEnding(code)) { effects.exit(attributeValueData) return valueQuotedBetween(code) @@ -305,6 +316,12 @@ export default function createAttributes ( return valueQuoted } + function valueQuotedEscape (code: number) { + effects.consume(code) + + return valueQuoted + } + function valueQuotedAfter (code: number) { return code === Codes.closingCurlyBracket || markdownLineEndingOrSpace(code) ? between(code) : end(code) } diff --git a/src/micromark-extension/types.ts b/src/micromark-extension/types.ts index 67e1d16..4e270d0 100644 --- a/src/micromark-extension/types.ts +++ b/src/micromark-extension/types.ts @@ -73,6 +73,8 @@ declare module 'micromark-util-types' { bindingContent: 'bindingContent', bindingFence: 'bindingFence', + escapeCharacter: 'escapeCharacter', + // Component Text componentText: 'componentText', componentTextMarker: 'componentTextMarker', diff --git a/src/to-markdown.ts b/src/to-markdown.ts index 9eecab5..ee3eb00 100644 --- a/src/to-markdown.ts +++ b/src/to-markdown.ts @@ -222,7 +222,7 @@ export default (opts: RemarkMDCOptions = {}) => { } else if (key.startsWith(':') && value === 'true') { values.push(key.slice(1)) } else if (key.startsWith(':') && isValidJSON(value)) { - values.push(`${key}='${value}'`) + values.push(`${key}='${value.replace(/([^/])'/g, '$1\\\'')}'`) } else { values.push(quoted(key, value)) } diff --git a/test/__snapshots__/block-component.test.ts.snap b/test/__snapshots__/block-component.test.ts.snap index 14d7759..5c94ec4 100644 --- a/test/__snapshots__/block-component.test.ts.snap +++ b/test/__snapshots__/block-component.test.ts.snap @@ -1057,6 +1057,53 @@ exports[`block-component > ignore-code-fence 1`] = ` } `; +exports[`block-component > jsonScapeAttr 1`] = ` +{ + "children": [ + { + "attributes": { + ":test": "{"foo":"I'd love to"}", + }, + "children": [], + "data": { + "hName": "foo", + "hProperties": { + ":test": "{"foo":"I'd love to"}", + }, + }, + "fmAttributes": {}, + "name": "foo", + "position": { + "end": { + "column": 3, + "line": 2, + "offset": 40, + }, + "start": { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "type": "containerComponent", + }, + ], + "position": { + "end": { + "column": 3, + "line": 2, + "offset": 40, + }, + "start": { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "type": "root", +} +`; + exports[`block-component > nested-component 1`] = ` { "children": [ diff --git a/test/block-component.test.ts b/test/block-component.test.ts index a8b3c47..17fbe99 100644 --- a/test/block-component.test.ts +++ b/test/block-component.test.ts @@ -29,6 +29,12 @@ describe('block-component', () => { markdown: '::with-frontmatter\n---\nkey: value\narray:\n - item\n - itemKey: value\n---\n::', expected: '::with-frontmatter\n---\narray:\n - item\n - itemKey: value\nkey: value\n---\n::' }, + jsonScapeAttr: { + markdown: '::foo{:test=\'{"foo":"I\\\'d love to"}\'}\n::', + extra: (_md, ast) => { + expect(ast.children[0].type).toBe('containerComponent') + } + }, frontmatter1: { markdown: [ '::with-frontmatter',