/** * @typedef {import('micromark-util-types').Construct} Construct * @typedef {import('micromark-util-types').Resolver} Resolver * @typedef {import('micromark-util-types').Tokenizer} Tokenizer * @typedef {import('micromark-util-types').State} State * @typedef {import('micromark-util-types').Code} Code */ import { asciiAlpha, asciiAlphanumeric, markdownLineEnding, markdownLineEndingOrSpace, markdownSpace } from 'micromark-util-character' import {htmlBlockNames, htmlRawNames} from 'micromark-util-html-tag-name' import {blankLine} from './blank-line.js' /** @type {Construct} */ export const htmlFlow = { name: 'htmlFlow', tokenize: tokenizeHtmlFlow, resolveTo: resolveToHtmlFlow, concrete: true } /** @type {Construct} */ const nextBlankConstruct = { tokenize: tokenizeNextBlank, partial: true } /** @type {Resolver} */ function resolveToHtmlFlow(events) { let index = events.length while (index--) { if (events[index][0] === 'enter' && events[index][1].type === 'htmlFlow') { break } } if (index > 1 && events[index - 2][1].type === 'linePrefix') { // Add the prefix start to the HTML token. events[index][1].start = events[index - 2][1].start // Add the prefix start to the HTML line token. events[index + 1][1].start = events[index - 2][1].start // Remove the line prefix. events.splice(index - 2, 2) } return events } /** @type {Tokenizer} */ function tokenizeHtmlFlow(effects, ok, nok) { const self = this /** @type {number} */ let kind /** @type {boolean} */ let startTag /** @type {string} */ let buffer /** @type {number} */ let index /** @type {Code} */ let marker return start /** @type {State} */ function start(code) { effects.enter('htmlFlow') effects.enter('htmlFlowData') effects.consume(code) return open } /** @type {State} */ function open(code) { if (code === 33) { effects.consume(code) return declarationStart } if (code === 47) { effects.consume(code) return tagCloseStart } if (code === 63) { effects.consume(code) kind = 3 // While we’re in an instruction instead of a declaration, we’re on a `?` // right now, so we do need to search for `>`, similar to declarations. return self.interrupt ? ok : continuationDeclarationInside } if (asciiAlpha(code)) { effects.consume(code) buffer = String.fromCharCode(code) startTag = true return tagName } return nok(code) } /** @type {State} */ function declarationStart(code) { if (code === 45) { effects.consume(code) kind = 2 return commentOpenInside } if (code === 91) { effects.consume(code) kind = 5 buffer = 'CDATA[' index = 0 return cdataOpenInside } if (asciiAlpha(code)) { effects.consume(code) kind = 4 return self.interrupt ? ok : continuationDeclarationInside } return nok(code) } /** @type {State} */ function commentOpenInside(code) { if (code === 45) { effects.consume(code) return self.interrupt ? ok : continuationDeclarationInside } return nok(code) } /** @type {State} */ function cdataOpenInside(code) { if (code === buffer.charCodeAt(index++)) { effects.consume(code) return index === buffer.length ? self.interrupt ? ok : continuation : cdataOpenInside } return nok(code) } /** @type {State} */ function tagCloseStart(code) { if (asciiAlpha(code)) { effects.consume(code) buffer = String.fromCharCode(code) return tagName } return nok(code) } /** @type {State} */ function tagName(code) { if ( code === null || code === 47 || code === 62 || markdownLineEndingOrSpace(code) ) { if ( code !== 47 && startTag && htmlRawNames.includes(buffer.toLowerCase()) ) { kind = 1 return self.interrupt ? ok(code) : continuation(code) } if (htmlBlockNames.includes(buffer.toLowerCase())) { kind = 6 if (code === 47) { effects.consume(code) return basicSelfClosing } return self.interrupt ? ok(code) : continuation(code) } kind = 7 // Do not support complete HTML when interrupting return self.interrupt && !self.parser.lazy[self.now().line] ? nok(code) : startTag ? completeAttributeNameBefore(code) : completeClosingTagAfter(code) } if (code === 45 || asciiAlphanumeric(code)) { effects.consume(code) buffer += String.fromCharCode(code) return tagName } return nok(code) } /** @type {State} */ function basicSelfClosing(code) { if (code === 62) { effects.consume(code) return self.interrupt ? ok : continuation } return nok(code) } /** @type {State} */ function completeClosingTagAfter(code) { if (markdownSpace(code)) { effects.consume(code) return completeClosingTagAfter } return completeEnd(code) } /** @type {State} */ function completeAttributeNameBefore(code) { if (code === 47) { effects.consume(code) return completeEnd } if (code === 58 || code === 95 || asciiAlpha(code)) { effects.consume(code) return completeAttributeName } if (markdownSpace(code)) { effects.consume(code) return completeAttributeNameBefore } return completeEnd(code) } /** @type {State} */ function completeAttributeName(code) { if ( code === 45 || code === 46 || code === 58 || code === 95 || asciiAlphanumeric(code) ) { effects.consume(code) return completeAttributeName } return completeAttributeNameAfter(code) } /** @type {State} */ function completeAttributeNameAfter(code) { if (code === 61) { effects.consume(code) return completeAttributeValueBefore } if (markdownSpace(code)) { effects.consume(code) return completeAttributeNameAfter } return completeAttributeNameBefore(code) } /** @type {State} */ function completeAttributeValueBefore(code) { if ( code === null || code === 60 || code === 61 || code === 62 || code === 96 ) { return nok(code) } if (code === 34 || code === 39) { effects.consume(code) marker = code return completeAttributeValueQuoted } if (markdownSpace(code)) { effects.consume(code) return completeAttributeValueBefore } marker = null return completeAttributeValueUnquoted(code) } /** @type {State} */ function completeAttributeValueQuoted(code) { if (code === null || markdownLineEnding(code)) { return nok(code) } if (code === marker) { effects.consume(code) return completeAttributeValueQuotedAfter } effects.consume(code) return completeAttributeValueQuoted } /** @type {State} */ function completeAttributeValueUnquoted(code) { if ( code === null || code === 34 || code === 39 || code === 60 || code === 61 || code === 62 || code === 96 || markdownLineEndingOrSpace(code) ) { return completeAttributeNameAfter(code) } effects.consume(code) return completeAttributeValueUnquoted } /** @type {State} */ function completeAttributeValueQuotedAfter(code) { if (code === 47 || code === 62 || markdownSpace(code)) { return completeAttributeNameBefore(code) } return nok(code) } /** @type {State} */ function completeEnd(code) { if (code === 62) { effects.consume(code) return completeAfter } return nok(code) } /** @type {State} */ function completeAfter(code) { if (markdownSpace(code)) { effects.consume(code) return completeAfter } return code === null || markdownLineEnding(code) ? continuation(code) : nok(code) } /** @type {State} */ function continuation(code) { if (code === 45 && kind === 2) { effects.consume(code) return continuationCommentInside } if (code === 60 && kind === 1) { effects.consume(code) return continuationRawTagOpen } if (code === 62 && kind === 4) { effects.consume(code) return continuationClose } if (code === 63 && kind === 3) { effects.consume(code) return continuationDeclarationInside } if (code === 93 && kind === 5) { effects.consume(code) return continuationCharacterDataInside } if (markdownLineEnding(code) && (kind === 6 || kind === 7)) { return effects.check( nextBlankConstruct, continuationClose, continuationAtLineEnding )(code) } if (code === null || markdownLineEnding(code)) { return continuationAtLineEnding(code) } effects.consume(code) return continuation } /** @type {State} */ function continuationAtLineEnding(code) { effects.exit('htmlFlowData') return htmlContinueStart(code) } /** @type {State} */ function htmlContinueStart(code) { if (code === null) { return done(code) } if (markdownLineEnding(code)) { return effects.attempt( { tokenize: htmlLineEnd, partial: true }, htmlContinueStart, done )(code) } effects.enter('htmlFlowData') return continuation(code) } /** @type {Tokenizer} */ function htmlLineEnd(effects, ok, nok) { return start /** @type {State} */ function start(code) { effects.enter('lineEnding') effects.consume(code) effects.exit('lineEnding') return lineStart } /** @type {State} */ function lineStart(code) { return self.parser.lazy[self.now().line] ? nok(code) : ok(code) } } /** @type {State} */ function continuationCommentInside(code) { if (code === 45) { effects.consume(code) return continuationDeclarationInside } return continuation(code) } /** @type {State} */ function continuationRawTagOpen(code) { if (code === 47) { effects.consume(code) buffer = '' return continuationRawEndTag } return continuation(code) } /** @type {State} */ function continuationRawEndTag(code) { if (code === 62 && htmlRawNames.includes(buffer.toLowerCase())) { effects.consume(code) return continuationClose } if (asciiAlpha(code) && buffer.length < 8) { effects.consume(code) buffer += String.fromCharCode(code) return continuationRawEndTag } return continuation(code) } /** @type {State} */ function continuationCharacterDataInside(code) { if (code === 93) { effects.consume(code) return continuationDeclarationInside } return continuation(code) } /** @type {State} */ function continuationDeclarationInside(code) { if (code === 62) { effects.consume(code) return continuationClose } // More dashes. if (code === 45 && kind === 2) { effects.consume(code) return continuationDeclarationInside } return continuation(code) } /** @type {State} */ function continuationClose(code) { if (code === null || markdownLineEnding(code)) { effects.exit('htmlFlowData') return done(code) } effects.consume(code) return continuationClose } /** @type {State} */ function done(code) { effects.exit('htmlFlow') return ok(code) } } /** @type {Tokenizer} */ function tokenizeNextBlank(effects, ok, nok) { return start /** @type {State} */ function start(code) { effects.exit('htmlFlowData') effects.enter('lineEndingBlank') effects.consume(code) effects.exit('lineEndingBlank') return effects.attempt(blankLine, ok, nok) } }