"use strict";
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
const vscode_languageserver_types_1 = require("vscode-languageserver-types");
const expand_full_1 = require("./expand/expand-full");
const extract = require("@emmetio/extract-abbreviation");
const path = require("path");
const fs = require("fs");
const JSONC = require("jsonc-parser");
const os_1 = require("os");
const data_1 = require("./data");
const snippetKeyCache = new Map();
let markupSnippetKeys;
let markupSnippetKeysRegex;
const stylesheetCustomSnippetsKeyCache = new Map();
const htmlAbbreviationStartRegex = /^[a-z,A-Z,!,(,[,#,\.]/;
const cssAbbreviationRegex = /^-?[a-z,A-Z,!,@,#]/;
const htmlAbbreviationRegex = /[a-z,A-Z\.]/;
const emmetModes = ['html', 'pug', 'slim', 'haml', 'xml', 'xsl', 'jsx', 'css', 'scss', 'sass', 'less', 'stylus'];
const commonlyUsedTags = [...data_1.htmlData.tags, 'lorem'];
const bemFilterSuffix = 'bem';
const filterDelimitor = '|';
const trimFilterSuffix = 't';
const commentFilterSuffix = 'c';
const maxFilters = 3;
const vendorPrefixes = { 'w': "webkit", 'm': "moz", 's': "ms", 'o': "o" };
const defaultVendorProperties = {
    'w': "animation, animation-delay, animation-direction, animation-duration, animation-fill-mode, animation-iteration-count, animation-name, animation-play-state, animation-timing-function, appearance, backface-visibility, background-clip, background-composite, background-origin, background-size, border-fit, border-horizontal-spacing, border-image, border-vertical-spacing, box-align, box-direction, box-flex, box-flex-group, box-lines, box-ordinal-group, box-orient, box-pack, box-reflect, box-shadow, color-correction, column-break-after, column-break-before, column-break-inside, column-count, column-gap, column-rule-color, column-rule-style, column-rule-width, column-span, column-width, dashboard-region, font-smoothing, highlight, hyphenate-character, hyphenate-limit-after, hyphenate-limit-before, hyphens, line-box-contain, line-break, line-clamp, locale, margin-before-collapse, margin-after-collapse, marquee-direction, marquee-increment, marquee-repetition, marquee-style, mask-attachment, mask-box-image, mask-box-image-outset, mask-box-image-repeat, mask-box-image-slice, mask-box-image-source, mask-box-image-width, mask-clip, mask-composite, mask-image, mask-origin, mask-position, mask-repeat, mask-size, nbsp-mode, perspective, perspective-origin, rtl-ordering, text-combine, text-decorations-in-effect, text-emphasis-color, text-emphasis-position, text-emphasis-style, text-fill-color, text-orientation, text-security, text-stroke-color, text-stroke-width, transform, transition, transform-origin, transform-style, transition-delay, transition-duration, transition-property, transition-timing-function, user-drag, user-modify, user-select, writing-mode, svg-shadow, box-sizing, border-radius",
    'm': "animation-delay, animation-direction, animation-duration, animation-fill-mode, animation-iteration-count, animation-name, animation-play-state, animation-timing-function, appearance, backface-visibility, background-inline-policy, binding, border-bottom-colors, border-image, border-left-colors, border-right-colors, border-top-colors, box-align, box-direction, box-flex, box-ordinal-group, box-orient, box-pack, box-shadow, box-sizing, column-count, column-gap, column-rule-color, column-rule-style, column-rule-width, column-width, float-edge, font-feature-settings, font-language-override, force-broken-image-icon, hyphens, image-region, orient, outline-radius-bottomleft, outline-radius-bottomright, outline-radius-topleft, outline-radius-topright, perspective, perspective-origin, stack-sizing, tab-size, text-blink, text-decoration-color, text-decoration-line, text-decoration-style, text-size-adjust, transform, transform-origin, transform-style, transition, transition-delay, transition-duration, transition-property, transition-timing-function, user-focus, user-input, user-modify, user-select, window-shadow, background-clip, border-radius",
    's': "accelerator, backface-visibility, background-position-x, background-position-y, behavior, block-progression, box-align, box-direction, box-flex, box-line-progression, box-lines, box-ordinal-group, box-orient, box-pack, content-zoom-boundary, content-zoom-boundary-max, content-zoom-boundary-min, content-zoom-chaining, content-zoom-snap, content-zoom-snap-points, content-zoom-snap-type, content-zooming, filter, flow-from, flow-into, font-feature-settings, grid-column, grid-column-align, grid-column-span, grid-columns, grid-layer, grid-row, grid-row-align, grid-row-span, grid-rows, high-contrast-adjust, hyphenate-limit-chars, hyphenate-limit-lines, hyphenate-limit-zone, hyphens, ime-mode, interpolation-mode, layout-flow, layout-grid, layout-grid-char, layout-grid-line, layout-grid-mode, layout-grid-type, line-break, overflow-style, perspective, perspective-origin, perspective-origin-x, perspective-origin-y, scroll-boundary, scroll-boundary-bottom, scroll-boundary-left, scroll-boundary-right, scroll-boundary-top, scroll-chaining, scroll-rails, scroll-snap-points-x, scroll-snap-points-y, scroll-snap-type, scroll-snap-x, scroll-snap-y, scrollbar-arrow-color, scrollbar-base-color, scrollbar-darkshadow-color, scrollbar-face-color, scrollbar-highlight-color, scrollbar-shadow-color, scrollbar-track-color, text-align-last, text-autospace, text-justify, text-kashida-space, text-overflow, text-size-adjust, text-underline-position, touch-action, transform, transform-origin, transform-origin-x, transform-origin-y, transform-origin-z, transform-style, transition, transition-delay, transition-duration, transition-property, transition-timing-function, user-select, word-break, wrap-flow, wrap-margin, wrap-through, writing-mode",
    'o': "dashboard-region, animation, animation-delay, animation-direction, animation-duration, animation-fill-mode, animation-iteration-count, animation-name, animation-play-state, animation-timing-function, border-image, link, link-source, object-fit, object-position, tab-size, table-baseline, transform, transform-origin, transition, transition-delay, transition-duration, transition-property, transition-timing-function, accesskey, input-format, input-required, marquee-dir, marquee-loop, marquee-speed, marquee-style"
};
/**
 * Returns all applicable emmet expansions for abbreviation at given position in a CompletionList
 * @param document TextDocument in which completions are requested
 * @param position Position in the document at which completions are requested
 * @param syntax Emmet supported language
 * @param emmetConfig Emmet Configurations as derived from VS Code
 */
function doComplete(document, position, syntax, emmetConfig) {
    if (emmetConfig.showExpandedAbbreviation === 'never' || !getEmmetMode(syntax, emmetConfig.excludeLanguages)) {
        return;
    }
    // Fetch markupSnippets so that we can provide possible abbreviation completions
    // For example, when text at position is `a`, completions should return `a:blank`, `a:link`, `acr` etc.
    if (!isStyleSheet(syntax)) {
        if (!snippetKeyCache.has(syntax) || !markupSnippetKeysRegex || markupSnippetKeysRegex.length === 0) {
            let registry = customSnippetRegistry[syntax] ? customSnippetRegistry[syntax] : expand_full_1.createSnippetsRegistry(syntax);
            if (!snippetKeyCache.has(syntax)) {
                snippetKeyCache.set(syntax, registry.all({ type: 'string' }).map(snippet => {
                    return snippet.key;
                }));
            }
            markupSnippetKeysRegex = registry.all({ type: 'regexp' }).map(snippet => {
                return snippet.key;
            });
        }
        markupSnippetKeys = snippetKeyCache.get(syntax);
    }
    let extractedValue = extractAbbreviation(document, position, { syntax, lookAhead: !isStyleSheet(syntax) });
    if (!extractedValue) {
        return;
    }
    let { abbreviationRange, abbreviation, filter } = extractedValue;
    let currentLineTillPosition = getCurrentLine(document, position).substr(0, position.character);
    let currentWord = getCurrentWord(currentLineTillPosition);
    // Dont attempt to expand open tags
    if (currentWord === abbreviation
        && currentLineTillPosition.endsWith(`<${abbreviation}`)
        && (syntax === 'html' || syntax === 'xml' || syntax === 'xsl' || syntax === 'jsx')) {
        return;
    }
    // `preferences` is supported in Emmet config to allow backward compatibility
    // `getExpandOptions` converts it into a format understandable by new modules
    // We retain a copy here to be used by the vendor prefixing feature
    let expandOptions = getExpandOptions(syntax, emmetConfig, filter);
    let preferences = expandOptions['preferences'];
    delete expandOptions['preferences'];
    let expandedText;
    let expandedAbbr;
    let completionItems = [];
    // Create completion item after expanding given abbreviation 
    // if abbreviation is valid and expanded value is not noise
    const createExpandedAbbr = (syntax, abbr) => {
        if (!isAbbreviationValid(syntax, abbreviation)) {
            return;
        }
        try {
            expandedText = expand_full_1.expand(abbr, expandOptions);
        }
        catch (e) {
        }
        if (!expandedText || isExpandedTextNoise(syntax, abbr, expandedText)) {
            return;
        }
        expandedAbbr = vscode_languageserver_types_1.CompletionItem.create(abbr);
        expandedAbbr.textEdit = vscode_languageserver_types_1.TextEdit.replace(abbreviationRange, escapeNonTabStopDollar(addFinalTabStop(expandedText)));
        expandedAbbr.documentation = replaceTabStopsWithCursors(expandedText);
        expandedAbbr.insertTextFormat = vscode_languageserver_types_1.InsertTextFormat.Snippet;
        expandedAbbr.detail = 'Emmet Abbreviation';
        expandedAbbr.label = abbreviation;
        expandedAbbr.label += filter ? '|' + filter.replace(',', '|') : "";
        completionItems = [expandedAbbr];
    };
    if (isStyleSheet(syntax)) {
        let { prefixOptions, abbreviationWithoutPrefix } = splitVendorPrefix(abbreviation);
        createExpandedAbbr(syntax, abbreviationWithoutPrefix);
        // When abbr is longer than usual emmet snippets and matches better with existing css property, then no emmet
        if (abbreviationWithoutPrefix.length > 4
            && data_1.cssData.properties.find(x => x.startsWith(abbreviationWithoutPrefix))) {
            return vscode_languageserver_types_1.CompletionList.create([], true);
        }
        if (expandedAbbr) {
            let prefixedExpandedText = applyVendorPrefixes(expandedText, prefixOptions, preferences);
            expandedAbbr.textEdit = vscode_languageserver_types_1.TextEdit.replace(abbreviationRange, escapeNonTabStopDollar(addFinalTabStop(prefixedExpandedText)));
            expandedAbbr.documentation = replaceTabStopsWithCursors(prefixedExpandedText);
            expandedAbbr.label = removeTabStops(expandedText);
            expandedAbbr.filterText = abbreviation;
            // Custom snippets should show up in completions if abbreviation is a prefix
            const stylesheetCustomSnippetsKeys = stylesheetCustomSnippetsKeyCache.has(syntax) ? stylesheetCustomSnippetsKeyCache.get(syntax) : stylesheetCustomSnippetsKeyCache.get('css');
            completionItems = makeSnippetSuggestion(stylesheetCustomSnippetsKeys, abbreviation, abbreviation, abbreviationRange, expandOptions, 'Emmet Custom Snippet', false);
            if (!completionItems.find(x => x.textEdit.newText === expandedAbbr.textEdit.newText)) {
                // Fix for https://github.com/Microsoft/vscode/issues/28933#issuecomment-309236902
                // When user types in propertyname, emmet uses it to match with snippet names, resulting in width -> widows or font-family -> font: family
                // Filter out those cases here.
                const abbrRegex = new RegExp('.*' + abbreviationWithoutPrefix.split('').map(x => (x === '$' || x === '+') ? '\\' + x : x).join('.*') + '.*', 'i');
                if (/\d/.test(abbreviation) || abbrRegex.test(expandedAbbr.label)) {
                    completionItems.push(expandedAbbr);
                }
            }
        }
        // Incomplete abbreviations that use vendor prefix 
        if (!completionItems.length && (abbreviation === '-' || /^-[wmso]{1,4}-?$/.test(abbreviation))) {
            return vscode_languageserver_types_1.CompletionList.create([], true);
        }
    }
    else {
        createExpandedAbbr(syntax, abbreviation);
        let tagToFindMoreSuggestionsFor = abbreviation;
        let newTagMatches = abbreviation.match(/(>|\+)([\w:-]+)$/);
        if (newTagMatches && newTagMatches.length === 3) {
            tagToFindMoreSuggestionsFor = newTagMatches[2];
        }
        let commonlyUsedTagSuggestions = makeSnippetSuggestion(commonlyUsedTags, tagToFindMoreSuggestionsFor, abbreviation, abbreviationRange, expandOptions, 'Emmet Abbreviation');
        completionItems = completionItems.concat(commonlyUsedTagSuggestions);
        if (emmetConfig.showAbbreviationSuggestions === true) {
            let abbreviationSuggestions = makeSnippetSuggestion(markupSnippetKeys.filter(x => !commonlyUsedTags.includes(x)), tagToFindMoreSuggestionsFor, abbreviation, abbreviationRange, expandOptions, 'Emmet Abbreviation');
            // Workaround for the main expanded abbr not appearing before the snippet suggestions
            if (expandedAbbr && abbreviationSuggestions.length > 0 && tagToFindMoreSuggestionsFor !== abbreviation) {
                expandedAbbr.sortText = '0' + expandedAbbr.label;
                abbreviationSuggestions.forEach(item => {
                    // Workaround for snippet suggestions items getting filtered out as the complete abbr does not start with snippetKey 
                    item.filterText = abbreviation;
                    // Workaround for the main expanded abbr not appearing before the snippet suggestions
                    item.sortText = '9' + abbreviation;
                });
            }
            completionItems = completionItems.concat(abbreviationSuggestions);
        }
    }
    if (emmetConfig.showSuggestionsAsSnippets === true) {
        completionItems.forEach(x => x.kind = vscode_languageserver_types_1.CompletionItemKind.Snippet);
    }
    return completionItems.length ? vscode_languageserver_types_1.CompletionList.create(completionItems, true) : undefined;
}
exports.doComplete = doComplete;
/**
 * Create & return snippets for snippet keys that start with given prefix
 */
function makeSnippetSuggestion(snippetKeys, prefix, abbreviation, abbreviationRange, expandOptions, snippetDetail, skipFullMatch = true) {
    if (!prefix || !snippetKeys) {
        return [];
    }
    let snippetCompletions = [];
    snippetKeys.forEach(snippetKey => {
        if (!snippetKey.startsWith(prefix.toLowerCase()) || (skipFullMatch && snippetKey === prefix.toLowerCase())) {
            return;
        }
        let currentAbbr = abbreviation + snippetKey.substr(prefix.length);
        let expandedAbbr;
        try {
            expandedAbbr = expand_full_1.expand(currentAbbr, expandOptions);
        }
        catch (e) {
        }
        if (!expandedAbbr) {
            return;
        }
        let item = vscode_languageserver_types_1.CompletionItem.create(prefix + snippetKey.substr(prefix.length));
        item.documentation = replaceTabStopsWithCursors(expandedAbbr);
        item.detail = snippetDetail;
        item.textEdit = vscode_languageserver_types_1.TextEdit.replace(abbreviationRange, escapeNonTabStopDollar(addFinalTabStop(expandedAbbr)));
        item.insertTextFormat = vscode_languageserver_types_1.InsertTextFormat.Snippet;
        snippetCompletions.push(item);
    });
    return snippetCompletions;
}
function getCurrentWord(currentLineTillPosition) {
    if (currentLineTillPosition) {
        let matches = currentLineTillPosition.match(/[\w,:,-,\.]*$/);
        if (matches) {
            return matches[0];
        }
    }
}
function replaceTabStopsWithCursors(expandedWord) {
    return expandedWord.replace(/([^\\])\$\{\d+\}/g, '$1|').replace(/\$\{\d+:([^\}]+)\}/g, '$1');
}
function removeTabStops(expandedWord) {
    return expandedWord.replace(/([^\\])\$\{\d+\}/g, '$1').replace(/\$\{\d+:([^\}]+)\}/g, '$1');
}
function escapeNonTabStopDollar(text) {
    return text ? text.replace(/([^\\])(\$)([^\{])/g, '$1\\$2$3') : text;
}
function addFinalTabStop(text) {
    if (!text || !text.trim()) {
        return text;
    }
    let maxTabStop = -1;
    let maxTabStopRanges = [];
    let foundLastStop = false;
    let replaceWithLastStop = false;
    let i = 0;
    let n = text.length;
    try {
        while (i < n && !foundLastStop) {
            // Look for ${
            if (text[i++] != '$' || text[i++] != '{') {
                continue;
            }
            // Find tabstop
            let numberStart = -1;
            let numberEnd = -1;
            while (i < n && /\d/.test(text[i])) {
                numberStart = numberStart < 0 ? i : numberStart;
                numberEnd = i + 1;
                i++;
            }
            // If ${ was not followed by a number and either } or :, then its not a tabstop
            if (numberStart === -1 || numberEnd === -1 || i >= n || (text[i] != '}' && text[i] != ':')) {
                continue;
            }
            // If ${0} was found, then break
            const currentTabStop = text.substring(numberStart, numberEnd);
            foundLastStop = currentTabStop === '0';
            if (foundLastStop) {
                break;
            }
            let foundPlaceholder = false;
            if (text[i++] == ':') {
                // TODO: Nested placeholders may break here
                while (i < n) {
                    if (text[i] == '}') {
                        foundPlaceholder = true;
                        break;
                    }
                    i++;
                }
            }
            // Decide to replace currentTabStop with ${0} only if its the max among all tabstops and is not a placeholder
            if (Number(currentTabStop) > Number(maxTabStop)) {
                maxTabStop = currentTabStop;
                maxTabStopRanges = [{ numberStart, numberEnd }];
                replaceWithLastStop = !foundPlaceholder;
            }
            else if (currentTabStop == maxTabStop) {
                maxTabStopRanges.push({ numberStart, numberEnd });
            }
        }
    }
    catch (e) {
    }
    if (replaceWithLastStop && !foundLastStop) {
        for (let i = 0; i < maxTabStopRanges.length; i++) {
            let rangeStart = maxTabStopRanges[i].numberStart;
            let rangeEnd = maxTabStopRanges[i].numberEnd;
            text = text.substr(0, rangeStart) + '0' + text.substr(rangeEnd);
        }
    }
    return text;
}
function getCurrentLine(document, position) {
    let offset = document.offsetAt(position);
    let text = document.getText();
    let start = 0;
    let end = text.length;
    for (let i = offset - 1; i >= 0; i--) {
        if (text[i] === '\n') {
            start = i + 1;
            break;
        }
    }
    for (let i = offset; i < text.length; i++) {
        if (text[i] === '\n') {
            end = i;
            break;
        }
    }
    return text.substring(start, end);
}
let customSnippetRegistry = {};
let variablesFromFile = {};
let profilesFromFile = {};
exports.emmetSnippetField = (index, placeholder) => `\${${index}${placeholder ? ':' + placeholder : ''}}`;
function isStyleSheet(syntax) {
    let stylesheetSyntaxes = ['css', 'scss', 'sass', 'less', 'stylus'];
    return (stylesheetSyntaxes.indexOf(syntax) > -1);
}
exports.isStyleSheet = isStyleSheet;
function getFilters(text, pos) {
    let filter;
    for (let i = 0; i < maxFilters; i++) {
        if (text.endsWith(`${filterDelimitor}${bemFilterSuffix}`, pos)) {
            pos -= bemFilterSuffix.length + 1;
            filter = filter ? bemFilterSuffix + ',' + filter : bemFilterSuffix;
        }
        else if (text.endsWith(`${filterDelimitor}${commentFilterSuffix}`, pos)) {
            pos -= commentFilterSuffix.length + 1;
            filter = filter ? commentFilterSuffix + ',' + filter : commentFilterSuffix;
        }
        else if (text.endsWith(`${filterDelimitor}${trimFilterSuffix}`, pos)) {
            pos -= trimFilterSuffix.length + 1;
            filter = filter ? trimFilterSuffix + ',' + filter : trimFilterSuffix;
        }
        else {
            break;
        }
    }
    return {
        pos: pos,
        filter: filter
    };
}
/**
 *  * Extracts abbreviation from the given position in the given document
 * @param document The TextDocument from which abbreviation needs to be extracted
 * @param position The Position in the given document from where abbreviation needs to be extracted
 * @param options The options to pass to the @emmetio/extract-abbreviation module
 */
function extractAbbreviation(document, position, options) {
    const currentLine = getCurrentLine(document, position);
    const currentLineTillPosition = currentLine.substr(0, position.character);
    const { pos, filter } = getFilters(currentLineTillPosition, position.character);
    const lengthOccupiedByFilter = filter ? filter.length + 1 : 0;
    try {
        let extractOptions = options;
        if (typeof extractOptions !== 'boolean') {
            extractOptions = extractOptions || {};
            extractOptions = {
                syntax: (isStyleSheet(extractOptions.syntax) || extractOptions.syntax === 'stylesheet') ? 'stylesheet' : 'markup',
                lookAhead: extractOptions.lookAhead
            };
        }
        const result = extract(currentLine, pos, extractOptions);
        const rangeToReplace = vscode_languageserver_types_1.Range.create(position.line, result.location, position.line, result.location + result.abbreviation.length + lengthOccupiedByFilter);
        return {
            abbreviationRange: rangeToReplace,
            abbreviation: result.abbreviation,
            filter
        };
    }
    catch (e) {
    }
}
exports.extractAbbreviation = extractAbbreviation;
/**
 * Extracts abbreviation from the given text
 * @param text Text from which abbreviation needs to be extracted
 * @param syntax Syntax used to extract the abbreviation from the given text
 */
function extractAbbreviationFromText(text, syntax) {
    if (!text) {
        return;
    }
    const { pos, filter } = getFilters(text, text.length);
    try {
        let extractOptions = (isStyleSheet(syntax) || syntax === 'stylesheet') ? { syntax: 'stylesheet', lookAhead: false } : true;
        const result = extract(text, pos, extractOptions);
        return {
            abbreviation: result.abbreviation,
            filter
        };
    }
    catch (e) {
    }
}
exports.extractAbbreviationFromText = extractAbbreviationFromText;
/**
 * Returns a boolean denoting validity of given abbreviation in the context of given syntax
 * Not needed once https://github.com/emmetio/atom-plugin/issues/22 is fixed
 * @param syntax string
 * @param abbreviation string
 */
function isAbbreviationValid(syntax, abbreviation) {
    if (!abbreviation) {
        return false;
    }
    if (isStyleSheet(syntax)) {
        // Fix for https://github.com/Microsoft/vscode/issues/1623 in new emmet
        if (abbreviation.endsWith(':')) {
            return false;
        }
        if (abbreviation.indexOf('#') > -1) {
            return hexColorRegex.test(abbreviation) || propertyHexColorRegex.test(abbreviation);
        }
        return cssAbbreviationRegex.test(abbreviation);
    }
    if (abbreviation.startsWith('!')) {
        return !/[^!]/.test(abbreviation);
    }
    const multipleMatch = abbreviation.match(/\*(\d+)$/);
    if (multipleMatch) {
        return parseInt(multipleMatch[1], 10) <= 100;
    }
    // Its common for users to type (sometextinsidebrackets), this should not be treated as an abbreviation
    // Grouping in abbreviation is valid only if preceeded/succeeded with one of the symbols for nesting, sibling, repeater or climb up
    if (!/\(.*\)[>\+\*\^]/.test(abbreviation) && !/[>\+\*\^]\(.*\)/.test(abbreviation) && /\(/.test(abbreviation) && /\)/.test(abbreviation)) {
        return false;
    }
    return (htmlAbbreviationStartRegex.test(abbreviation) && htmlAbbreviationRegex.test(abbreviation));
}
exports.isAbbreviationValid = isAbbreviationValid;
function isExpandedTextNoise(syntax, abbreviation, expandedText) {
    // Unresolved css abbreviations get expanded to a blank property value
    // Eg: abc -> abc: ; or abc:d -> abc: d; which is noise if it gets suggested for every word typed
    if (isStyleSheet(syntax)) {
        let after = (syntax === 'sass' || syntax === 'stylus') ? '' : ';';
        return expandedText === `${abbreviation}: \${1}${after}` || expandedText.replace(/\s/g, '') === abbreviation.replace(/\s/g, '') + after;
    }
    if (commonlyUsedTags.indexOf(abbreviation.toLowerCase()) > -1 || markupSnippetKeys.indexOf(abbreviation) > -1) {
        return false;
    }
    // Custom tags can have - or :
    if (/[-,:]/.test(abbreviation) && !/--|::/.test(abbreviation) && !abbreviation.endsWith(':')) {
        return false;
    }
    // Its common for users to type some text and end it with period, this should not be treated as an abbreviation
    // Else it becomes noise.
    // When user just types '.', return the expansion
    // Otherwise emmet loses change to participate later
    // For example in `.foo`. See https://github.com/Microsoft/vscode/issues/66013
    if (abbreviation === '.') {
        return false;
    }
    const dotMatches = abbreviation.match(/^([a-z,A-Z,\d]*)\.$/);
    if (dotMatches) {
        // Valid html tags such as `div.`
        if (dotMatches[1] && data_1.htmlData.tags.includes(dotMatches[1])) {
            return false;
        }
        return true;
    }
    // Unresolved html abbreviations get expanded as if it were a tag
    // Eg: abc -> <abc></abc> which is noise if it gets suggested for every word typed
    return (expandedText.toLowerCase() === `<${abbreviation.toLowerCase()}>\${1}</${abbreviation.toLowerCase()}>`);
}
/**
 * Returns options to be used by the @emmetio/expand-abbreviation module
 * @param syntax
 * @param textToReplace
 */
function getExpandOptions(syntax, emmetConfig, filter) {
    emmetConfig = emmetConfig || {};
    emmetConfig['preferences'] = emmetConfig['preferences'] || {};
    // Fetch snippet registry
    let baseSyntax = isStyleSheet(syntax) ? 'css' : 'html';
    if (!customSnippetRegistry[syntax] && customSnippetRegistry[baseSyntax]) {
        customSnippetRegistry[syntax] = customSnippetRegistry[baseSyntax];
    }
    // Fetch Profile
    let profile = getProfile(syntax, emmetConfig['syntaxProfiles']);
    let filtersFromProfile = (profile && profile['filters']) ? profile['filters'].split(',') : [];
    filtersFromProfile = filtersFromProfile.map(filterFromProfile => filterFromProfile.trim());
    // Update profile based on preferences
    if (emmetConfig['preferences']['format.noIndentTags']) {
        if (Array.isArray(emmetConfig['preferences']['format.noIndentTags'])) {
            profile['formatSkip'] = emmetConfig['preferences']['format.noIndentTags'];
        }
        else if (typeof emmetConfig['preferences']['format.noIndentTags'] === 'string') {
            profile['formatSkip'] = emmetConfig['preferences']['format.noIndentTags'].split(',');
        }
    }
    if (emmetConfig['preferences']['format.forceIndentationForTags']) {
        if (Array.isArray(emmetConfig['preferences']['format.forceIndentationForTags'])) {
            profile['formatForce'] = emmetConfig['preferences']['format.forceIndentationForTags'];
        }
        else if (typeof emmetConfig['preferences']['format.forceIndentationForTags'] === 'string') {
            profile['formatForce'] = emmetConfig['preferences']['format.forceIndentationForTags'].split(',');
        }
    }
    if (emmetConfig['preferences']['profile.allowCompactBoolean'] && typeof emmetConfig['preferences']['profile.allowCompactBoolean'] === 'boolean') {
        profile['compactBooleanAttributes'] = emmetConfig['preferences']['profile.allowCompactBoolean'];
    }
    // Fetch Add Ons
    let addons = {};
    if (filter && filter.split(',').find(x => x.trim() === 'bem') || filtersFromProfile.indexOf('bem') > -1) {
        addons['bem'] = { element: '__' };
        if (emmetConfig['preferences']['bem.elementSeparator']) {
            addons['bem']['element'] = emmetConfig['preferences']['bem.elementSeparator'];
        }
        if (emmetConfig['preferences']['bem.modifierSeparator']) {
            addons['bem']['modifier'] = emmetConfig['preferences']['bem.modifierSeparator'];
        }
    }
    if (syntax === 'jsx') {
        addons['jsx'] = true;
    }
    // Fetch Formatters
    let formatters = getFormatters(syntax, emmetConfig['preferences']);
    if (filter && filter.split(',').find(x => x.trim() === 'c') || filtersFromProfile.indexOf('c') > -1) {
        if (!formatters['comment']) {
            formatters['comment'] = {
                enabled: true
            };
        }
        else {
            formatters['comment']['enabled'] = true;
        }
    }
    // If the user doesn't provide specific properties for a vendor, use the default values
    let preferences = emmetConfig['preferences'];
    for (const v in vendorPrefixes) {
        let vendorProperties = preferences['css.' + vendorPrefixes[v] + 'Properties'];
        if (vendorProperties == null) {
            preferences['css.' + vendorPrefixes[v] + 'Properties'] = defaultVendorProperties[v];
        }
    }
    return {
        field: exports.emmetSnippetField,
        syntax: syntax,
        profile: profile,
        addons: addons,
        variables: getVariables(emmetConfig['variables']),
        snippets: customSnippetRegistry[syntax],
        format: formatters,
        preferences: preferences
    };
}
exports.getExpandOptions = getExpandOptions;
function splitVendorPrefix(abbreviation) {
    abbreviation = abbreviation || "";
    if (abbreviation[0] != '-') {
        return {
            prefixOptions: "",
            abbreviationWithoutPrefix: abbreviation
        };
    }
    else {
        abbreviation = abbreviation.substr(1);
        let pref = "-";
        if (/^[wmso]*-./.test(abbreviation)) {
            let index = abbreviation.indexOf("-");
            if (index > -1) {
                pref += abbreviation.substr(0, index + 1);
                abbreviation = abbreviation.substr(index + 1);
            }
        }
        return {
            prefixOptions: pref,
            abbreviationWithoutPrefix: abbreviation
        };
    }
}
function applyVendorPrefixes(expandedProperty, vendors, preferences) {
    preferences = preferences || {};
    expandedProperty = expandedProperty || "";
    vendors = vendors || "";
    if (vendors[0] !== '-') {
        return expandedProperty;
    }
    if (vendors == "-") {
        let defaultVendors = "-";
        let property = expandedProperty.substr(0, expandedProperty.indexOf(':'));
        if (!property) {
            return expandedProperty;
        }
        for (const v in vendorPrefixes) {
            let vendorProperties = preferences['css.' + vendorPrefixes[v] + 'Properties'];
            if (vendorProperties && vendorProperties.split(',').find(x => x.trim() === property))
                defaultVendors += v;
        }
        // If no vendors specified, add all
        vendors = defaultVendors == "-" ? "-wmso" : defaultVendors;
        vendors += '-';
    }
    vendors = vendors.substr(1);
    let prefixedProperty = "";
    for (let index = 0; index < vendors.length - 1; index++) {
        prefixedProperty += '-' + vendorPrefixes[vendors[index]] + '-' + expandedProperty + "\n";
    }
    return prefixedProperty + expandedProperty;
}
/**
 * Parses given abbreviation using given options and returns a tree
 * @param abbreviation string
 * @param options options used by the @emmetio/expand-abbreviation module to parse given abbreviation
 */
function parseAbbreviation(abbreviation, options) {
    return expand_full_1.parse(abbreviation, options);
}
exports.parseAbbreviation = parseAbbreviation;
/**
 * Expands given abbreviation using given options
 * @param abbreviation string or parsed abbreviation
 * @param options options used by the @emmetio/expand-abbreviation module to expand given abbreviation
 */
function expandAbbreviation(abbreviation, options) {
    let expandedText;
    let preferences = options['preferences'];
    delete options['preferences'];
    if (isStyleSheet(options['syntax']) && typeof abbreviation === 'string') {
        let { prefixOptions, abbreviationWithoutPrefix } = splitVendorPrefix(abbreviation);
        expandedText = expand_full_1.expand(abbreviationWithoutPrefix, options);
        expandedText = applyVendorPrefixes(expandedText, prefixOptions, preferences);
    }
    else {
        expandedText = expand_full_1.expand(abbreviation, options);
    }
    return escapeNonTabStopDollar(addFinalTabStop(expandedText));
}
exports.expandAbbreviation = expandAbbreviation;
/**
 * Maps and returns syntaxProfiles of previous format to ones compatible with new emmet modules
 * @param syntax
 */
function getProfile(syntax, profilesFromSettings) {
    if (!profilesFromSettings) {
        profilesFromSettings = {};
    }
    let profilesConfig = Object.assign({}, profilesFromFile, profilesFromSettings);
    let options = profilesConfig[syntax];
    if (!options || typeof options === 'string') {
        if (options === 'xhtml') {
            return {
                selfClosingStyle: 'xhtml'
            };
        }
        return {};
    }
    let newOptions = {};
    for (let key in options) {
        switch (key) {
            case 'tag_case':
                newOptions['tagCase'] = (options[key] === 'lower' || options[key] === 'upper') ? options[key] : '';
                break;
            case 'attr_case':
                newOptions['attributeCase'] = (options[key] === 'lower' || options[key] === 'upper') ? options[key] : '';
                break;
            case 'attr_quotes':
                newOptions['attributeQuotes'] = options[key];
                break;
            case 'tag_nl':
                newOptions['format'] = (options[key] === true || options[key] === false) ? options[key] : true;
                break;
            case 'inline_break':
                newOptions['inlineBreak'] = options[key];
                break;
            case 'self_closing_tag':
                if (options[key] === true) {
                    newOptions['selfClosingStyle'] = 'xml';
                    break;
                }
                if (options[key] === false) {
                    newOptions['selfClosingStyle'] = 'html';
                    break;
                }
                newOptions['selfClosingStyle'] = options[key];
                break;
            case 'compact_bool':
                newOptions['compactBooleanAttributes'] = options[key];
                break;
            default:
                newOptions[key] = options[key];
                break;
        }
    }
    return newOptions;
}
/**
 * Returns variables to be used while expanding snippets
 */
function getVariables(variablesFromSettings) {
    if (!variablesFromSettings) {
        return variablesFromFile;
    }
    return Object.assign({}, variablesFromFile, variablesFromSettings);
}
function getFormatters(syntax, preferences) {
    if (!preferences) {
        return {};
    }
    if (!isStyleSheet(syntax)) {
        let commentFormatter = {};
        for (let key in preferences) {
            switch (key) {
                case 'filter.commentAfter':
                    commentFormatter['after'] = preferences[key];
                    break;
                case 'filter.commentBefore':
                    commentFormatter['before'] = preferences[key];
                    break;
                case 'filter.commentTrigger':
                    commentFormatter['trigger'] = preferences[key];
                    break;
                default:
                    break;
            }
        }
        return {
            comment: commentFormatter
        };
    }
    let fuzzySearchMinScore = typeof preferences['css.fuzzySearchMinScore'] === 'number' ? preferences['css.fuzzySearchMinScore'] : 0.3;
    if (fuzzySearchMinScore > 1) {
        fuzzySearchMinScore = 1;
    }
    else if (fuzzySearchMinScore < 0) {
        fuzzySearchMinScore = 0;
    }
    let stylesheetFormatter = {
        'fuzzySearchMinScore': fuzzySearchMinScore
    };
    for (let key in preferences) {
        switch (key) {
            case 'css.floatUnit':
                stylesheetFormatter['floatUnit'] = preferences[key];
                break;
            case 'css.intUnit':
                stylesheetFormatter['intUnit'] = preferences[key];
                break;
            case 'css.unitAliases':
                let unitAliases = {};
                preferences[key].split(',').forEach(alias => {
                    if (!alias || !alias.trim() || alias.indexOf(':') === -1) {
                        return;
                    }
                    let aliasName = alias.substr(0, alias.indexOf(':'));
                    let aliasValue = alias.substr(aliasName.length + 1);
                    if (!aliasName.trim() || !aliasValue) {
                        return;
                    }
                    unitAliases[aliasName.trim()] = aliasValue;
                });
                stylesheetFormatter['unitAliases'] = unitAliases;
                break;
            case `${syntax}.valueSeparator`:
                stylesheetFormatter['between'] = preferences[key];
                break;
            case `${syntax}.propertyEnd`:
                stylesheetFormatter['after'] = preferences[key];
                break;
            default:
                break;
        }
    }
    return {
        stylesheet: stylesheetFormatter
    };
}
/**
 * Updates customizations from snippets.json and syntaxProfiles.json files in the directory configured in emmet.extensionsPath setting
 */
function updateExtensionsPath(emmetExtensionsPath, workspaceFolderPath) {
    if (!emmetExtensionsPath || !emmetExtensionsPath.trim()) {
        resetSettingsFromFile();
        return Promise.resolve();
    }
    emmetExtensionsPath = emmetExtensionsPath.trim();
    workspaceFolderPath = workspaceFolderPath ? workspaceFolderPath.trim() : '';
    if (emmetExtensionsPath[0] === '~') {
        emmetExtensionsPath = path.join(os_1.homedir(), emmetExtensionsPath.substr(1));
    }
    else if (!path.isAbsolute(emmetExtensionsPath) && workspaceFolderPath) {
        emmetExtensionsPath = path.join(workspaceFolderPath, emmetExtensionsPath);
    }
    if (!path.isAbsolute(emmetExtensionsPath)) {
        resetSettingsFromFile();
        return Promise.reject('The path provided in emmet.extensionsPath setting should be absoulte path');
    }
    if (!dirExists(emmetExtensionsPath)) {
        resetSettingsFromFile();
        return Promise.reject(`The directory ${emmetExtensionsPath} doesnt exist. Update emmet.extensionsPath setting`);
    }
    let dirPath = emmetExtensionsPath;
    let snippetsPath = path.join(dirPath, 'snippets.json');
    let profilesPath = path.join(dirPath, 'syntaxProfiles.json');
    let snippetsPromise = new Promise((resolve, reject) => {
        fs.readFile(snippetsPath, (err, snippetsData) => {
            if (err) {
                return reject(`Error while fetching the file ${snippetsPath}`);
            }
            try {
                let errors = [];
                let snippetsJson = JSONC.parse(snippetsData.toString(), errors);
                if (errors.length > 0) {
                    return reject(`Found error ${JSONC.ScanError[errors[0].error]} while parsing the file ${snippetsPath} at offset ${errors[0].offset}`);
                }
                variablesFromFile = snippetsJson['variables'];
                customSnippetRegistry = {};
                snippetKeyCache.clear();
                Object.keys(snippetsJson).forEach(syntax => {
                    if (!snippetsJson[syntax]['snippets']) {
                        return;
                    }
                    let baseSyntax = isStyleSheet(syntax) ? 'css' : 'html';
                    let customSnippets = snippetsJson[syntax]['snippets'];
                    if (snippetsJson[baseSyntax] && snippetsJson[baseSyntax]['snippets'] && baseSyntax !== syntax) {
                        customSnippets = Object.assign({}, snippetsJson[baseSyntax]['snippets'], snippetsJson[syntax]['snippets']);
                    }
                    if (!isStyleSheet(syntax)) {
                        // In Emmet 2.0 all snippets should be valid abbreviations
                        // Convert old snippets that do not follow this format to new format
                        for (let snippetKey in customSnippets) {
                            if (customSnippets.hasOwnProperty(snippetKey)
                                && customSnippets[snippetKey].startsWith('<')
                                && customSnippets[snippetKey].endsWith('>')) {
                                customSnippets[snippetKey] = `{${customSnippets[snippetKey]}}`;
                            }
                        }
                    }
                    else {
                        stylesheetCustomSnippetsKeyCache.set(syntax, Object.keys(customSnippets));
                    }
                    customSnippetRegistry[syntax] = expand_full_1.createSnippetsRegistry(syntax, customSnippets);
                    let snippetKeys = customSnippetRegistry[syntax].all({ type: 'string' }).map(snippet => {
                        return snippet.key;
                    });
                    snippetKeyCache.set(syntax, snippetKeys);
                });
            }
            catch (e) {
                return reject(`Error while parsing the file ${snippetsPath}`);
            }
            return resolve();
        });
    });
    let variablesPromise = new Promise((resolve, reject) => {
        fs.readFile(profilesPath, (err, profilesData) => {
            try {
                if (!err) {
                    profilesFromFile = JSON.parse(profilesData.toString());
                }
            }
            catch (e) {
            }
            return resolve();
        });
    });
    return Promise.all([snippetsPromise, variablesFromFile]).then(() => Promise.resolve());
}
exports.updateExtensionsPath = updateExtensionsPath;
function dirExists(dirPath) {
    try {
        return fs.statSync(dirPath).isDirectory();
    }
    catch (e) {
        return false;
    }
}
function resetSettingsFromFile() {
    customSnippetRegistry = {};
    snippetKeyCache.clear();
    stylesheetCustomSnippetsKeyCache.clear();
    profilesFromFile = {};
    variablesFromFile = {};
}
/**
* Get the corresponding emmet mode for given vscode language mode
* Eg: jsx for typescriptreact/javascriptreact or pug for jade
* If the language is not supported by emmet or has been exlcuded via `exlcudeLanguages` setting,
* then nothing is returned
*
* @param language
* @param exlcudedLanguages Array of language ids that user has chosen to exlcude for emmet
*/
function getEmmetMode(language, excludedLanguages = []) {
    if (!language || excludedLanguages.indexOf(language) > -1) {
        return;
    }
    if (/\b(typescriptreact|javascriptreact|jsx-tags)\b/.test(language)) { // treat tsx like jsx
        return 'jsx';
    }
    if (language === 'sass-indented') { // map sass-indented to sass
        return 'sass';
    }
    if (language === 'jade') {
        return 'pug';
    }
    if (emmetModes.indexOf(language) > -1) {
        return language;
    }
}
exports.getEmmetMode = getEmmetMode;
const propertyHexColorRegex = /^[a-zA-Z]+:?#[\d.a-fA-F]{0,6}$/;
const hexColorRegex = /^#[\d,a-f,A-F]{1,6}$/;
const onlyLetters = /^[a-z,A-Z]+$/;
/**
 * Returns a completion participant for Emmet of the form {
 * 		onCssProperty: () => void
 * 		onCssPropertyValue: () => void
 * 		onHtmlContent: () => void
 * }
 * @param document The TextDocument for which completions are being provided
 * @param position The Position in the given document where completions are being provided
 * @param syntax The Emmet syntax to use when providing Emmet completions
 * @param emmetSettings The Emmet settings to use when providing Emmet completions
 * @param result The Completion List object that needs to be updated with Emmet completions
 */
function getEmmetCompletionParticipants(document, position, syntax, emmetSettings, result) {
    return {
        getId: () => 'emmet',
        onCssProperty: (context) => {
            if (context && context.propertyName) {
                const currentresult = doComplete(document, position, syntax, emmetSettings);
                if (result && currentresult) {
                    result.items = currentresult.items;
                    result.isIncomplete = true;
                }
            }
        },
        onCssPropertyValue: (context) => {
            if (context && context.propertyValue) {
                const extractedResults = extractAbbreviation(document, position, { syntax: 'css', lookAhead: false });
                if (!extractedResults) {
                    return;
                }
                const validAbbreviationWithColon = extractedResults.abbreviation === `${context.propertyName}:${context.propertyValue}` && onlyLetters.test(context.propertyValue);
                if (validAbbreviationWithColon // Allows abbreviations like pos:f
                    || hexColorRegex.test(extractedResults.abbreviation)
                    || extractedResults.abbreviation === '!') {
                    const currentresult = doComplete(document, position, syntax, emmetSettings);
                    if (result && currentresult) {
                        result.items = currentresult.items;
                        result.isIncomplete = true;
                    }
                }
            }
        },
        onHtmlContent: () => {
            const currentresult = doComplete(document, position, syntax, emmetSettings);
            if (result && currentresult) {
                result.items = currentresult.items;
                result.isIncomplete = true;
            }
        }
    };
}
exports.getEmmetCompletionParticipants = getEmmetCompletionParticipants;
//# sourceMappingURL=emmetHelper.js.map