|
- /**
- * Minimalistic backwards stream reader
- */
- class StreamReader {
- constructor(string) {
- this.string = string;
- this.pos = this.string.length;
- }
-
- sol() {
- return this.pos === 0;
- }
-
- peek(offset) {
- return this.string.charCodeAt(this.pos - 1 + (offset || 0));
- }
-
- prev() {
- if (!this.sol()) {
- return this.string.charCodeAt(--this.pos);
- }
- }
-
- eat(match) {
- const ok = typeof match === 'function'
- ? match(this.peek())
- : match === this.peek();
-
- if (ok) {
- this.pos--;
- }
-
- return ok;
- }
-
- eatWhile(match) {
- const start = this.pos;
- while (this.eat(match)) {}
- return this.pos < start;
- }
- }
-
- /**
- * Quotes-related utilities
- */
-
- const SINGLE_QUOTE = 39; // '
- const DOUBLE_QUOTE = 34; // "
- const ESCAPE = 92; // \
-
- /**
- * Check if given character code is a quote
- * @param {Number} c
- * @return {Boolean}
- */
- function isQuote(c) {
- return c === SINGLE_QUOTE || c === DOUBLE_QUOTE;
- }
-
- /**
- * Consumes quoted value, if possible
- * @param {StreamReader} stream
- * @return {Boolean} Returns `true` is value was consumed
- */
- function eatQuoted(stream) {
- const start = stream.pos;
- const quote = stream.prev();
-
- if (isQuote(quote)) {
- while (!stream.sol()) {
- if (stream.prev() === quote && stream.peek() !== ESCAPE) {
- return true;
- }
- }
- }
-
- stream.pos = start;
- return false;
- }
-
- const TAB = 9;
- const SPACE = 32;
- const DASH = 45; // -
- const SLASH = 47; // /
- const COLON = 58; // :
- const EQUALS = 61; // =
- const ANGLE_LEFT = 60; // <
- const ANGLE_RIGHT = 62; // >
-
- /**
- * Check if given reader’s current position points at the end of HTML tag
- * @param {StreamReader} stream
- * @return {Boolean}
- */
- var isAtHTMLTag = function (stream) {
- const start = stream.pos;
-
- if (!stream.eat(ANGLE_RIGHT)) {
- return false;
- }
-
- let ok = false;
- stream.eat(SLASH); // possibly self-closed element
-
- while (!stream.sol()) {
- stream.eatWhile(isWhiteSpace);
-
- if (eatIdent(stream)) {
- // ate identifier: could be a tag name, boolean attribute or unquoted
- // attribute value
- if (stream.eat(SLASH)) {
- // either closing tag or invalid tag
- ok = stream.eat(ANGLE_LEFT);
- break;
- } else if (stream.eat(ANGLE_LEFT)) {
- // opening tag
- ok = true;
- break;
- } else if (stream.eat(isWhiteSpace)) {
- // boolean attribute
- continue;
- } else if (stream.eat(EQUALS)) {
- // simple unquoted value or invalid attribute
- if (eatIdent(stream)) {
- continue;
- }
- break;
- } else if (eatAttributeWithUnquotedValue(stream)) {
- // identifier was a part of unquoted value
- ok = true;
- break;
- }
-
- // invalid tag
- break;
- }
-
- if (eatAttribute(stream)) {
- continue;
- }
-
- break;
- }
-
- stream.pos = start;
- return ok;
- };
-
- /**
- * Eats HTML attribute from given string.
- * @param {StreamReader} state
- * @return {Boolean} `true` if attribute was consumed.
- */
- function eatAttribute(stream) {
- return eatAttributeWithQuotedValue(stream) || eatAttributeWithUnquotedValue(stream);
- }
-
- /**
- * @param {StreamReader} stream
- * @return {Boolean}
- */
- function eatAttributeWithQuotedValue(stream) {
- const start = stream.pos;
- if (eatQuoted(stream) && stream.eat(EQUALS) && eatIdent(stream)) {
- return true;
- }
-
- stream.pos = start;
- return false;
- }
-
- /**
- * @param {StreamReader} stream
- * @return {Boolean}
- */
- function eatAttributeWithUnquotedValue(stream) {
- const start = stream.pos;
- if (stream.eatWhile(isUnquotedValue) && stream.eat(EQUALS) && eatIdent(stream)) {
- return true;
- }
-
- stream.pos = start;
- return false;
- }
-
- /**
- * Eats HTML identifier from stream
- * @param {StreamReader} stream
- * @return {Boolean}
- */
- function eatIdent(stream) {
- return stream.eatWhile(isIdent);
- }
-
- /**
- * Check if given character code belongs to HTML identifier
- * @param {Number} c
- * @return {Boolean}
- */
- function isIdent(c) {
- return c === COLON || c === DASH || isAlpha(c) || isNumber(c);
- }
-
- /**
- * Check if given character code is alpha code (letter though A to Z)
- * @param {Number} c
- * @return {Boolean}
- */
- function isAlpha(c) {
- c &= ~32; // quick hack to convert any char code to uppercase char code
- return c >= 65 && c <= 90; // A-Z
- }
-
- /**
- * Check if given code is a number
- * @param {Number} c
- * @return {Boolean}
- */
- function isNumber(c) {
- return c > 47 && c < 58;
- }
-
- /**
- * Check if given code is a whitespace
- * @param {Number} c
- * @return {Boolean}
- */
- function isWhiteSpace(c) {
- return c === SPACE || c === TAB;
- }
-
- /**
- * Check if given code may belong to unquoted attribute value
- * @param {Number} c
- * @return {Boolean}
- */
- function isUnquotedValue(c) {
- return c && c !== EQUALS && !isWhiteSpace(c) && !isQuote(c);
- }
-
- const code = ch => ch.charCodeAt(0);
- const SQUARE_BRACE_L = code('[');
- const SQUARE_BRACE_R = code(']');
- const ROUND_BRACE_L = code('(');
- const ROUND_BRACE_R = code(')');
- const CURLY_BRACE_L = code('{');
- const CURLY_BRACE_R = code('}');
-
- const specialChars = new Set('#.*:$-_!@%^+>/'.split('').map(code));
- const bracePairs = new Map()
- .set(SQUARE_BRACE_L, SQUARE_BRACE_R)
- .set(ROUND_BRACE_L, ROUND_BRACE_R)
- .set(CURLY_BRACE_L, CURLY_BRACE_R);
-
- const defaultOptions = {
- syntax: 'markup',
- lookAhead: null
- };
-
- /**
- * Extracts Emmet abbreviation from given string.
- * The goal of this module is to extract abbreviation from current editor’s line,
- * e.g. like this: `<span>.foo[title=bar|]</span>` -> `.foo[title=bar]`, where
- * `|` is a current caret position.
- * @param {String} line A text line where abbreviation should be expanded
- * @param {Number} [pos] Caret position in line. If not given, uses end-of-line
- * @param {Object} [options]
- * @param {Boolean} [options.lookAhead] Allow parser to look ahead of `pos` index for
- * searching of missing abbreviation parts. Most editors automatically inserts
- * closing braces for `[`, `{` and `(`, which will most likely be right after
- * current caret position. So in order to properly expand abbreviation, user
- * must explicitly move caret right after auto-inserted braces. Whith this option
- * enabled, parser will search for closing braces right after `pos`. Default is `true`
- * @param {String} [options.syntax] Name of context syntax of expanded abbreviation.
- * Either 'markup' (default) or 'stylesheet'. In 'stylesheet' syntax, braces `[]`
- * and `{}` are not supported thus not extracted.
- * @return {Object} Object with `abbreviation` and its `location` in given line
- * if abbreviation can be extracted, `null` otherwise
- */
- function extractAbbreviation(line, pos, options) {
- // make sure `pos` is within line range
- pos = Math.min(line.length, Math.max(0, pos == null ? line.length : pos));
-
- if (typeof options === 'boolean') {
- options = Object.assign(defaultOptions, { lookAhead: options });
- } else {
- options = Object.assign(defaultOptions, options);
- }
-
- if (options.lookAhead == null || options.lookAhead === true) {
- pos = offsetPastAutoClosed(line, pos, options);
- }
-
- let c;
- const stream = new StreamReader(line);
- stream.pos = pos;
- const stack = [];
-
- while (!stream.sol()) {
- c = stream.peek();
-
- if (isCloseBrace(c, options.syntax)) {
- stack.push(c);
- } else if (isOpenBrace(c, options.syntax)) {
- if (stack.pop() !== bracePairs.get(c)) {
- // unexpected brace
- break;
- }
- } else if (has(stack, SQUARE_BRACE_R) || has(stack, CURLY_BRACE_R)) {
- // respect all characters inside attribute sets or text nodes
- stream.pos--;
- continue;
- } else if (isAtHTMLTag(stream) || !isAbbreviation(c)) {
- break;
- }
-
- stream.pos--;
- }
-
- if (!stack.length && stream.pos !== pos) {
- // found something, remove some invalid symbols from the
- // beginning and return abbreviation
- const abbreviation = line.slice(stream.pos, pos).replace(/^[*+>^]+/, '');
- return {
- abbreviation,
- location: pos - abbreviation.length
- };
- }
- }
-
- /**
- * Returns new `line` index which is right after characters beyound `pos` that
- * edditor will likely automatically close, e.g. }, ], and quotes
- * @param {String} line
- * @param {Number} pos
- * @return {Number}
- */
- function offsetPastAutoClosed(line, pos, options) {
- // closing quote is allowed only as a next character
- if (isQuote(line.charCodeAt(pos))) {
- pos++;
- }
-
- // offset pointer until non-autoclosed character is found
- while (isCloseBrace(line.charCodeAt(pos), options.syntax)) {
- pos++;
- }
-
- return pos;
- }
-
- function has(arr, value) {
- return arr.indexOf(value) !== -1;
- }
-
- function isAbbreviation(c) {
- return (c > 64 && c < 91) // uppercase letter
- || (c > 96 && c < 123) // lowercase letter
- || (c > 47 && c < 58) // number
- || specialChars.has(c); // special character
- }
-
- function isOpenBrace(c, syntax) {
- return c === ROUND_BRACE_L || (syntax === 'markup' && (c === SQUARE_BRACE_L || c === CURLY_BRACE_L));
- }
-
- function isCloseBrace(c, syntax) {
- return c === ROUND_BRACE_R || (syntax === 'markup' && (c === SQUARE_BRACE_R || c === CURLY_BRACE_R));
- }
-
- export default extractAbbreviation;
|