import Scanner, { eatPair, isAlpha, isNumber, isQuote, isSpace, eatQuoted } from '@emmetio/scanner'; const defaultOptions = { xml: false, allTokens: false, special: { style: null, script: ['', 'text/javascript', 'application/x-javascript', 'javascript', 'typescript', 'ts', 'coffee', 'coffeescript'] }, empty: ['img', 'meta', 'link', 'br', 'base', 'hr', 'area', 'wbr', 'col', 'embed', 'input', 'param', 'source', 'track'] }; /** Options for `Scanner` utils */ const opt = { throws: false }; function createOptions(options = {}) { return Object.assign(Object.assign({}, defaultOptions), options); } /** * Converts given string into array of character codes */ function toCharCodes(str) { return str.split('').map(ch => ch.charCodeAt(0)); } /** * Consumes array of character codes from given scanner */ function consumeArray(scanner, codes) { const start = scanner.pos; for (let i = 0; i < codes.length; i++) { if (!scanner.eat(codes[i])) { scanner.pos = start; return false; } } scanner.start = start; return true; } /** * Consumes section from given string which starts with `open` character codes * and ends with `close` character codes * @return Returns `true` if section was consumed */ function consumeSection(scanner, open, close, allowUnclosed) { const start = scanner.pos; if (consumeArray(scanner, open)) { // consumed `'); const piStart = toCharCodes(''); const erbStart = toCharCodes('<%'); const erbEnd = toCharCodes('%>'); /** * Performs fast scan of given source code: for each tag found it invokes callback * with tag name, its type (open, close, self-close) and range in original source. * Unlike regular scanner, fast scanner doesn’t provide info about attributes to * reduce object allocations hence increase performance. * If `callback` returns `false`, scanner stops parsing. * @param special List of “special” HTML tags which should be ignored. Most likely * it’s a "script" and "style" tags. */ function scan(source, callback, options) { const scanner = new Scanner(source); const special = options ? options.special : null; const allTokens = options ? options.allTokens : false; let type; let name; let nameStart; let nameEnd; let nameCodes; let found = false; let piName = null; while (!scanner.eof()) { const start = scanner.pos; if (cdata(scanner)) { if (allTokens && callback('#cdata', 4 /* CData */, scanner.start, scanner.pos) === false) { break; } } else if (comment(scanner)) { if (allTokens && callback('#comment', 6 /* Comment */, scanner.start, scanner.pos) === false) { break; } } else if (erb(scanner)) { if (allTokens && callback('#erb', 7 /* ERB */, scanner.start, scanner.pos) === false) { break; } } else if (piName = processingInstruction(scanner)) { if (allTokens && callback(piName, 5 /* ProcessingInstruction */, scanner.start, scanner.pos) === false) { break; } } else if (scanner.eat(60 /* LeftAngle */)) { // Maybe a tag name? type = scanner.eat(47 /* Slash */) ? 2 /* Close */ : 1 /* Open */; nameStart = scanner.pos; if (ident(scanner)) { // Consumed tag name nameEnd = scanner.pos; if (type !== 2 /* Close */) { skipAttributes(scanner); scanner.eatWhile(isSpace); if (scanner.eat(47 /* Slash */)) { type = 3 /* SelfClose */; } } if (scanner.eat(62 /* RightAngle */)) { // Tag properly closed name = scanner.substring(nameStart, nameEnd); if (callback(name, type, start, scanner.pos) === false) { break; } if (type === 1 /* Open */ && special && isSpecial(special, name, source, start, scanner.pos)) { // Found opening tag of special element: we should skip // scanner contents until we find closing tag nameCodes = toCharCodes(name); found = false; while (!scanner.eof()) { if (consumeClosing(scanner, nameCodes)) { found = true; break; } scanner.pos++; } if (found && callback(name, 2 /* Close */, scanner.start, scanner.pos) === false) { break; } } } } } else { scanner.pos++; } } } /** * Skips attributes in current tag context */ function skipAttributes(scanner) { while (!scanner.eof()) { scanner.eatWhile(isSpace); if (attributeName(scanner)) { if (scanner.eat(61 /* Equals */)) { attributeValue(scanner); } } else if (isTerminator(scanner.peek())) { break; } else { scanner.pos++; } } } /** * Consumes closing tag with given name from scanner */ function consumeClosing(scanner, name) { const start = scanner.pos; if (scanner.eat(60 /* LeftAngle */) && scanner.eat(47 /* Slash */) && consumeArray(scanner, name) && scanner.eat(62 /* RightAngle */)) { scanner.start = start; return true; } scanner.pos = start; return false; } /** * Consumes CDATA from given scanner */ function cdata(scanner) { return consumeSection(scanner, cdataOpen, cdataClose, true); } /** * Consumes comments from given scanner */ function comment(scanner) { return consumeSection(scanner, commentOpen, commentClose, true); } /** * Consumes processing instruction from given scanner. If consumed, returns * processing instruction name */ function processingInstruction(scanner) { const start = scanner.pos; if (consumeArray(scanner, piStart) && ident(scanner)) { const name = scanner.current(); while (!scanner.eof()) { if (consumeArray(scanner, piEnd)) { break; } eatQuoted(scanner) || scanner.pos++; } scanner.start = start; return name; } scanner.pos = start; return null; } /** * Consumes ERB-style entity: `<% ... %>` or `<%= ... %>` */ function erb(scanner) { const start = scanner.pos; if (consumeArray(scanner, erbStart)) { while (!scanner.eof()) { if (consumeArray(scanner, erbEnd)) { break; } eatQuoted(scanner) || scanner.pos++; } scanner.start = start; return true; } scanner.pos = start; return false; } /** * Check if given tag name should be considered as special */ function isSpecial(special, name, source, start, end) { if (name in special) { const typeValues = special[name]; if (!Array.isArray(typeValues)) { return true; } const attrs = attributes(source.substring(start + name.length + 1, end - 1)); return typeValues.includes(getAttributeValue(attrs, 'type') || ''); } return false; } /** * Finds matched tag for given `pos` location in XML/HTML `source` */ function match(source, pos, opt) { // Since we expect large input document, we’ll use pooling technique // for storing tag data to reduce memory pressure and improve performance const pool = []; const stack = []; const options = createOptions(opt); let result = null; scan(source, (name, type, start, end) => { if (type === 1 /* Open */ && isSelfClose(name, options)) { // Found empty element in HTML mode, mark is as self-closing type = 3 /* SelfClose */; } if (type === 1 /* Open */) { // Allocate tag object from pool stack.push(allocTag(pool, name, start, end)); } else if (type === 3 /* SelfClose */) { if (start < pos && pos < end) { // Matched given self-closing tag result = { name, attributes: getAttributes(source, start, end, name), open: [start, end] }; return false; } } else { const tag = last(stack); if (tag && tag.name === name) { // Matching closing tag found if (tag.start < pos && pos < end) { result = { name, attributes: getAttributes(source, tag.start, tag.end, name), open: [tag.start, tag.end], close: [start, end] }; return false; } else if (stack.length) { // Release tag object for further re-use releaseTag(pool, stack.pop()); } } } }, options); stack.length = pool.length = 0; return result; } /** * Returns balanced tag model: a list of all XML/HTML tags that could possibly match * given location when moving in outward direction */ function balancedOutward(source, pos, opt) { const pool = []; const stack = []; const options = createOptions(opt); const result = []; scan(source, (name, type, start, end) => { if (type === 2 /* Close */) { const tag = last(stack); if (tag && tag.name === name) { // XXX check for invalid tag names? // Matching closing tag found, check if matched pair is a candidate // for outward balancing if (tag.start < pos && pos < end) { result.push({ name, open: [tag.start, tag.end], close: [start, end] }); } // Release tag object for further re-use releaseTag(pool, stack.pop()); } } else if (type === 3 /* SelfClose */ || isSelfClose(name, options)) { if (start < pos && pos < end) { // Matched self-closed tag result.push({ name, open: [start, end] }); } } else { stack.push(allocTag(pool, name, start, end)); } }, options); stack.length = pool.length = 0; return result; } /** * Returns balanced tag model: a list of all XML/HTML tags that could possibly match * given location when moving in inward direction */ function balancedInward(source, pos, opt) { // Collecting tags for inward balancing is a bit trickier: we have to store // first child of every matched tag until we find the one that matches given // location const pool = []; const stack = []; const options = createOptions(opt); const result = []; const alloc = (name, start, end) => { if (pool.length) { const tag = pool.pop(); tag.name = name; tag.ranges.push(start, end); return tag; } return { name, ranges: [start, end] }; }; const release = (tag) => { tag.ranges.length = 0; tag.firstChild = void 0; pool.push(tag); }; scan(source, (name, type, start, end) => { if (type === 2 /* Close */) { if (!stack.length) { // Some sort of lone closing tag, ignore it return; } let tag = last(stack); if (tag.name === name) { // XXX check for invalid tag names? // Matching closing tag found, check if matched pair is a candidate // for outward balancing if (tag.ranges[0] <= pos && pos <= end) { result.push({ name, open: tag.ranges.slice(0, 2), close: [start, end] }); while (tag.firstChild) { const child = tag.firstChild; const res = { name: child.name, open: child.ranges.slice(0, 2) }; if (child.ranges.length > 2) { res.close = child.ranges.slice(2, 4); } result.push(res); release(tag); tag = child; } return false; } else { stack.pop(); const parent = last(stack); if (parent && !parent.firstChild) { // No first child in parent node: store current tag tag.ranges.push(start, end); parent.firstChild = tag; } else { release(tag); } } } } else if (type === 3 /* SelfClose */ || isSelfClose(name, options)) { if (start < pos && pos < end) { // Matched self-closed tag, no need to look further result.push({ name, open: [start, end] }); return false; } const parent = last(stack); if (parent && !parent.firstChild) { parent.firstChild = alloc(name, start, end); } } else { stack.push(alloc(name, start, end)); } }, options); stack.length = pool.length = 0; return result; } function allocTag(pool, name, start, end) { if (pool.length) { const tag = pool.pop(); tag.name = name; tag.start = start; tag.end = end; return tag; } return { name, start, end }; } function releaseTag(pool, tag) { pool.push(tag); } /** * Returns parsed attributes from given source */ function getAttributes(source, start, end, name) { const tokens = attributes(source.slice(start, end), name); tokens.forEach(attr => { attr.nameStart += start; attr.nameEnd += start; if (attr.value != null) { attr.valueStart += start; attr.valueEnd += start; } }); return tokens; } /** * Check if given tag is self-close for current parsing context */ function isSelfClose(name, options) { return !options.xml && options.empty.includes(name); } function last(arr) { return arr.length ? arr[arr.length - 1] : null; } export default match; export { attributes, balancedInward, balancedOutward, createOptions, scan }; //# sourceMappingURL=html-matcher.es.js.map