@shopify/theme-language-server-common
Version:
<h1 align="center" style="position: relative;" > <br> <img src="https://github.com/Shopify/theme-check-vscode/blob/main/images/shopify_glyph.png?raw=true" alt="logo" width="141" height="160"> <br> Theme Language Server </h1>
239 lines • 7.74 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.fix = exports.CURSOR = void 0;
exports.CURSOR = '█';
const SINGLE_QUOTE = `'`;
const DOUBLE_QUOTE = `"`;
const HTML_TOKENS = ['<', '>'];
const SHOULD_IGNORE_HTML_TOKENS = [SINGLE_QUOTE, DOUBLE_QUOTE, '{{', '{%'];
const QUOTES = [SINGLE_QUOTE, DOUBLE_QUOTE];
const CONTROL_TOKENS = ['{{', '{%', '<'];
const TokenPairs = {
"'": "'",
'"': '"',
'{{': '}}',
'{%': '%}',
'<': '>',
'[': ']',
'(': ')',
};
/**
* Fix the source code and return the new fixed source with the new absolute
* position
*
* @param source - source code
* @param position - absolute position
*
* @returns new fixed source
*/
function fix(source, position = source.length) {
const fixer = new Fixer(source, position);
return fixer.fix();
}
exports.fix = fix;
class Fixer {
constructor(source, position) {
/**
* A list of quotes, <, [, etc. We'll pass through that list to determine
* what needs to be closed at the end.
*/
this.tokens = [];
/**
* A stack of closing tokens such that when we're done we simply pop
* everything out into the string to get a fixed string.
*/
this.stack = [];
this.cursor = 0;
this.markup = source.slice(0, position);
}
/**
* This is cool, so bear with me.
*
* We'll scan the entire string up to the cursor position and turn that
* into a list of tokens
*
* input:
* `<a href="hi'" other="{{ 'there'`
*
* output:
* - <
* - "
* - '
* - "
* - "
* - {{
* - '
* - '
* - }}
* - "
*
* Then we take that output, and we turn it into a stack
* (open tokens push, close tokens pop)
*
* stack evolution:
* - < # open tag
* - < " # add quote
* - < " # (single quote is ignored)
* - < # close quote
* - < " # open new quote
* - < " {{ # open liquid variable output
* - < " {{ ' # open liquid string in variable output
* - < " {{ # close liquid string in variable output
*
* then we pop the close characters of that stack onto the string and
* have a fixed string
*
* - <a href="hi'" other="{{ 'there' # start
* - <a href="hi'" other="{{ 'there'}} # pop close {{
* - <a href="hi'" other="{{ 'there'}}" # pop close "
* - <a href="hi'" other="{{ 'there'}}"> # pop close <
*
* And there we go, we have a fixed string.
*/
fix() {
this.scanTokens();
this.buildStack();
let markup = this.markup;
if (this.shouldIncludeCursorPlaceholder()) {
markup += exports.CURSOR;
}
while (this.stack.length !== 0) {
markup += this.stack.pop();
}
return markup;
}
buildStack() {
for (let token of this.tokens) {
if (this.shouldPanic(token)) {
while (token !== this.stack.pop()) { }
}
else if (this.shouldSkipToken(token)) {
/* do nothing */
}
else if (this.isClosingToken(token)) {
this.stack.pop();
}
else {
const closingToken = TokenPairs[token];
if (closingToken) {
this.stack.push(closingToken);
}
}
}
}
isClosingToken(token) {
return this.current === token;
}
shouldPanic(token) {
const isInStringContext = QUOTES.includes(this.current);
return isInStringContext && this.stack.at(-2) === token;
}
shouldSkipToken(token) {
const current = this.current;
const isInTextContext = !current;
const isInStringContext = QUOTES.includes(current);
const isInLiquidContext = this.stack.includes('}}') || this.stack.includes('%}');
return ((isInTextContext && !CONTROL_TOKENS.includes(token)) ||
(isInStringContext && !CONTROL_TOKENS.includes(token) && !QUOTES.includes(token)) ||
(isInLiquidContext && token === '<') ||
(current === SINGLE_QUOTE && token === DOUBLE_QUOTE) ||
(current === DOUBLE_QUOTE && token === SINGLE_QUOTE) ||
(SHOULD_IGNORE_HTML_TOKENS.includes(current) && HTML_TOKENS.includes(token)));
}
scanTokens() {
while (!this.isAtEnd()) {
const character = this.peek();
switch (character) {
case DOUBLE_QUOTE:
case SINGLE_QUOTE:
case '(':
case ')':
case '[':
case ']': {
this.pushToken(character);
break;
}
case '{': {
if (this.matchNext('%')) {
this.pushToken('{%');
}
else if (this.matchNext('{')) {
this.pushToken('{{');
}
break;
}
case '%': {
if (this.matchNext('}')) {
this.pushToken('%}');
}
break;
}
case '}': {
if (this.matchNext('}')) {
this.pushToken('}}');
}
break;
}
case '<': {
if (this.testNext(/[a-z{\/]/i)) {
this.pushToken('<');
}
break;
}
case '>': {
this.pushToken('>');
break;
}
default: {
/* do nothing */
}
}
this.advance();
}
}
shouldIncludeCursorPlaceholder() {
var _a, _b;
const prevCharacter = (_a = this.markup.at(-1)) !== null && _a !== void 0 ? _a : '';
const prevPrevCharacter = (_b = this.markup.at(-2)) !== null && _b !== void 0 ? _b : '';
const isInLiquidContext = this.stack.includes('%}') || this.stack.includes('}}');
const isInHtmlContext = this.current === '>';
const isInStringContext = QUOTES.includes(this.current);
return ((isInStringContext && this.stack.at(-2) === '>' && QUOTES.includes(prevCharacter)) ||
(isInHtmlContext &&
(/\s/.test(prevCharacter) ||
prevCharacter === '<' ||
(prevPrevCharacter === '<' && prevCharacter === '/') ||
(prevPrevCharacter === '%' && prevCharacter === '}'))) ||
(isInLiquidContext &&
((!isInStringContext && [' ', '\t', '\n', '.', '{', '[', ','].includes(prevCharacter)) ||
(isInStringContext && prevPrevCharacter === '['))));
}
get current() {
return this.stack.at(-1);
}
advance() {
this.cursor += 1;
}
matchNext(character) {
if (this.peekNext() !== character)
return false;
this.cursor++;
return true;
}
testNext(regex) {
return regex.test(this.peekNext());
}
peek(cursor = this.cursor) {
return this.markup[cursor];
}
peekNext(cursor = this.cursor) {
return this.markup[cursor + 1];
}
pushToken(token) {
this.tokens.push(token);
}
isAtEnd() {
return this.cursor >= this.markup.length;
}
}
//# sourceMappingURL=fix.js.map
;