[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_style = space\nindent_size = 4\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.{json,yml}]\nindent_style = space\nindent_size = 2\n\n[snippets/*.json]\nindent_style = tab\nindent_size = 4\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: emmetio\npatreon: # Replace with a single Patreon username\nopen_collective: emmet\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/workflows/node.js.yml",
    "content": "# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node\n# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs\n\nname: Node.js CI\n\non:\n  push:\n    branches: [ \"master\", \"ci\" ]\n  pull_request:\n    branches: [ \"master\" ]\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        node-version: [20.x, 22.x, 24.x]\n        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/\n\n    steps:\n    - uses: actions/checkout@v3\n    - name: Use Node.js ${{ matrix.node-version }}\n      uses: actions/setup-node@v4\n      with:\n        node-version: ${{ matrix.node-version }}\n    - run: npm ci\n    - run: npm run build:full\n    - run: npm run test:all\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript v1 declaration files\ntypings/\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional REPL history\n.node_repl_history\n.rpt2_cache\ndist/\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n\n# next.js build output\n.next\n.DS_Store\n/.vscode\n"
  },
  {
    "path": ".npmignore",
    "content": "npm-debug.log*\nnode_modules\njspm_packages\n.npm\n/.*\n/*.*\n/test\n/src\n/packages\n"
  },
  {
    "path": ".npmrc",
    "content": "save-exact=true\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Sergey Chikuyonok <serge.che@gmail.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Emmet — the essential toolkit for web-developers\n\nEmmet is a web-developer’s toolkit for boosting HTML & CSS code writing.\n\nWith Emmet, you can type expressions (_abbreviations_) similar to CSS selectors and convert them into code fragment with a single keystroke. For example, this abbreviation:\n\n```\nul#nav>li.item$*4>a{Item $}\n```\n\n...can be expanded into:\n\n```html\n<ul id=\"nav\">\n    <li class=\"item1\"><a href=\"\">Item 1</a></li>\n    <li class=\"item2\"><a href=\"\">Item 2</a></li>\n    <li class=\"item3\"><a href=\"\">Item 3</a></li>\n    <li class=\"item4\"><a href=\"\">Item 4</a></li>\n</ul>\n```\n\n## Features\n\n* **Familiar syntax**: as a web-developer, you already know how to use Emmet. Abbreviation syntax is similar to CSS Selectors with shortcuts for id, class, custom attributes, element nesting and so on.\n* **Dynamic snippets**: unlike default editor snippets, Emmet abbreviations are dynamic and parsed as-you-type. No need to predefine them for each project, just type `MyComponent>custom-element` to convert any word into a tag.\n* **CSS properties shortcuts**: Emmet provides special syntax for CSS properties with embedded values. For example, `bd1-s#f.5` will be expanded to `border: 1px solid rgba(255, 255, 255, 0.5)`.\n* **Available for most popular syntaxes**: use single abbreviation to produce code for most popular syntaxes like HAML, Pug, JSX, SCSS, SASS etc.\n\n[Read more about Emmet features](https://docs.emmet.io)\n\nThis repo contains only core module for parsing and expanding Emmet abbreviations. Editor plugins are available as [separate repos](https://github.com/emmetio).\n\nThis is a *monorepo*: top-level project contains all the code required for converting abbreviation into code fragment while [`./packages`](/packages) folder contains modules for parsing abbreviations into AST and can be used independently (for example, as lexer for syntax highlighting).\n\n### Installation\n\nYou can install Emmet as a regular npm module:\n\n```bash\nnpm i emmet\n```\n\n## Usage\n\nTo expand abbreviation, pass it to default function of `emmet` module:\n\n```js\nimport expand from 'emmet';\n\nconsole.log(expand('p>a')); // <p><a href=\"\"></a></p>\n```\n\nBy default, Emmet expands *markup* abbreviation, e.g. abbreviation used for producing nested elements with attributes (like HTML, XML, HAML etc.). If you want to expand *stylesheet* abbreviation, you should pass it as a `type` property of second argument:\n\n```js\nimport expand from 'emmet';\n\nconsole.log(expand('p10', { type: 'stylesheet' })); // padding: 10px;\n```\n\nA stylesheet abbreviation has slightly different syntax compared to markup one: it doesn’t support nesting and attributes but allows embedded values in element name.\n\nAlternatively, Emmet supports *syntaxes* with predefined snippets and options:\n\n```js\nimport expand from 'emmet';\n\nconsole.log(expand('p10', { syntax: 'css' })); // padding: 10px;\nconsole.log(expand('p10', { syntax: 'stylus' })); // padding 10px\n```\n\nPredefined syntaxes already have `type` attribute which describes whether given abbreviation is markup or stylesheet, but if you want to use it with your custom syntax name, you should provide `type` config option as well (default is `markup`):\n\n```js\nimport expand from 'emmet';\n\nconsole.log(expand('p10', {\n    syntax: 'my-custom-syntax',\n    type: 'stylesheet',\n    options: {\n        'stylesheet.between': '__',\n        'stylesheet.after': '',\n    }\n})); // padding__10px\n```\n\nYou can pass `options` property as well to shape-up final output or enable/disable various features. See [`src/config.ts`](src/config.ts) for more info and available options.\n\n## Extracting abbreviations from text\n\nA common workflow with Emmet is to type abbreviation somewhere in source code and then expand it with editor action. To support such workflow, abbreviations must be properly _extracted_ from source code:\n\n```js\nimport expand, { extract } from 'emmet';\n\nconst source = 'Hello world ul.tabs>li';\nconst data = extract(source, 22); // { abbreviation: 'ul.tabs>li' }\n\nconsole.log(expand(data.abbreviation)); // <ul class=\"tabs\"><li></li></ul>\n```\n\nThe `extract` function accepts source code (most likely, current line) and character location in source from which abbreviation search should be started. The abbreviation is searched in backward direction: the location pointer is moved backward until it finds abbreviation bound. Returned result is an object with `abbreviation` property and `start` and `end` properties which describe location of extracted abbreviation in given source.\n\nMost current editors automatically insert closing quote or bracket for `(`, `[` and `{` characters so when user types abbreviation that uses attributes or text, it will end with the following state (`|` is caret location):\n\n```\nul>li[title=\"Foo|\"]\n```\n\nE.g. caret location is not at the end of abbreviation and must be moved a few characters ahead. The `extract` function is able to handle such cases with `lookAhead` option (enabled by default). This this option enabled, `extract` method automatically detects auto-inserted characters and adjusts location, which will be available as `end` property of the returned result:\n\n```js\nimport { extract } from 'emmet';\n\nconst source = 'a div[title] b';\nconst loc = 11; // right after \"title\" word\n\n// `lookAhead` is enabled by default\nconsole.log(extract(source, loc)); // { abbreviation: 'div[title]', start: 2, end: 12 }\nconsole.log(extract(source, loc, { lookAhead: false })); // { abbreviation: 'title', start: 6, end: 11 }\n```\n\nBy default, `extract` tries to detect _markup_ abbreviations (see above). _stylesheet_ abbreviations has slightly different syntax so in order to extract abbreviations for stylesheet syntaxes like CSS, you should pass `type: 'stylesheet'` option:\n\n```js\nimport { extract } from 'emmet';\n\nconst source = 'a{b}';\nconst loc = 3; // right after \"b\"\n\nconsole.log(extract(source, loc)); // { abbreviation: 'a{b}', start: 0, end: 4 }\n\n\n// Stylesheet abbreviations does not have `{text}` syntax\nconsole.log(extract(source, loc, { type: 'stylesheet' })); // { abbreviation: 'b', start: 2, end: 3 }\n```\n\n### Extract abbreviation with custom prefix\n\nLots of developers uses React (or similar) library for writing UI code which mixes JS and XML (JSX) in the same source code. Since _any_ Latin word can be used as Emmet abbreviation, writing JSX code with Emmet becomes pain since it will interfere with native editor snippets and distract user with false positive abbreviation matches for variable names, methods etc.:\n\n```js\nvar div // `div` is a valid abbreviation, Emmet may transform it to `<div></div>`\n```\n\nA possible solution for this problem it to use _prefix_ for abbreviation: abbreviation can be successfully extracted only if its preceded with given prefix.\n\n```js\nimport { extract } from 'emmet';\n\nconst source1 = '() => div';\nconst source2 = '() => <div';\n\nextract(source1, source1.length); // Finds `div` abbreviation\nextract(source2, source2.length); // Finds `div` abbreviation too\n\nextract(source1, source1.length, { prefix: '<' }); // No match, `div` abbreviation is not preceded with `<` prefix\nextract(source2, source2.length, { prefix: '<' }); // Finds `div` since it preceded with `<` prefix\n```\n\nWith `prefix` option, you can customize your experience with Emmet in any common syntax (HTML, CSS and so on) if user is distracted too much with Emmet completions for any typed word. A `prefix` may contain multiple character but the last one *must* be a character which is not part of Emmet abbreviation. Good candidates are `<`, `&`, `→` (emoji or Unicode symbol) and so on.\n"
  },
  {
    "path": "lerna.json",
    "content": "{\n  \"$schema\": \"node_modules/lerna/schemas/lerna-schema.json\",\n  \"version\": \"independent\"\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"emmet\",\n  \"version\": \"2.4.11\",\n  \"description\": \"Emmet — the essential toolkit for web-developers\",\n  \"main\": \"./dist/emmet.cjs\",\n  \"module\": \"./dist/emmet.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"type\": \"module\",\n  \"exports\": {\n    \"import\": \"./dist/emmet.js\",\n    \"require\": \"./dist/emmet.cjs\"\n  },\n  \"scripts\": {\n    \"build\": \"rollup -c\",\n    \"build:packages\": \"npm run build --if-present --workspaces\",\n    \"build:full\": \"npm run build:packages && npm run build\",\n    \"watch\": \"rollup -wc\",\n    \"test\": \"tsx --test ./test/*.ts\",\n    \"test:all\": \"npm run test --workspaces && npm run test\",\n    \"clean\": \"rimraf ./dist\",\n    \"prepublishOnly\": \"npm run clean && npm run build:full\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/emmetio/emmet.git\"\n  },\n  \"keywords\": [\n    \"emmet\",\n    \"html\",\n    \"css\",\n    \"snippets\",\n    \"coding\"\n  ],\n  \"author\": \"Sergey Chikuyonok <serge.che@gmail.com>\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/emmetio/emmet/issues\"\n  },\n  \"homepage\": \"https://github.com/emmetio/emmet#readme\",\n  \"devDependencies\": {\n    \"@emmetio/abbreviation\": \"workspace:\",\n    \"@emmetio/css-abbreviation\": \"workspace:\",\n    \"@rollup/plugin-node-resolve\": \"16.0.3\",\n    \"@rollup/plugin-typescript\": \"12.2.0\",\n    \"@types/node\": \"22.10.1\",\n    \"lerna\": \"9.0.0\",\n    \"rimraf\": \"6.0.1\",\n    \"rollup\": \"4.52.5\",\n    \"tsx\": \"4.20.6\",\n    \"typescript\": \"5.9.3\"\n  },\n  \"workspaces\": [\n    \"./packages/scanner\",\n    \"./packages/abbreviation\",\n    \"./packages/css-abbreviation\"\n  ]\n}\n"
  },
  {
    "path": "packages/abbreviation/.npmignore",
    "content": "npm-debug.log*\nnode_modules\njspm_packages\n.npm\n/.*\n/*.*\n/test\n/src\n"
  },
  {
    "path": "packages/abbreviation/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Sergey Chikuyonok <serge.che@gmail.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/abbreviation/README.md",
    "content": "# Emmet markup abbreviation parser\n\nParses given Emmet *markup* abbreviation into AST. Parsing is performed in two steps: first it tokenizes given abbreviation (useful for syntax highlighting in editors) and then tokens are analyzed and converted into AST nodes as plain, JSON-serializable objects.\n\nNote that AST tree in most cases cannot be used directly for output: for example, AST node produced from `.foo.bar` element misses element name and contains two `class` attributes with `foo` and `bar` values (not a single `class` with `foo bar` value).\n\n## Usage\n\nYou can install it via npm:\n\n```bash\nnpm install @emmetio/abbreviation\n```\n\nThen add it into your project:\n\n```js\nimport parse from '@emmetio/abbreviation';\n\nconst tree = parse('div#foo>span.bar*3');\n/* {\n    type: 'Abbreviation',\n    children: [{\n        type: 'AbbreviationNode',\n        name: 'div',\n        attributes: [...],\n        children: [...]\n    }]\n} */\n\n```\nThe returned tree contains `AbbreviationNode` items: a node with name, attributes and/or text content. E.g. an element that can be represented somehow. Repeated and grouped nodes like `a>(b+c)*3` are automatically converted and duplicated as distinct `AbbreviationNode` with distinct `.repeat` property which identifies node in repeating sequence.\n\n## Abbreviation syntax\n\nEmmet abbreviation element has the following basic parts:\n\n```\nname.class#id[attributes?, ...]{text value}*repeater/\n```\n\n* `name` — element name, like `div`, `span` etc. Stored as `node.name` property.\n* `[attributes]` — list of attributes. Each attribute is stored as [`AbbreviationAttribute`](/src/types.ts) instance and can be accessed by `node.getAttribute(name)`. Each attribute can be written in different formats:\n\t* `attr` — attribute with empty value.\n\t* `attr=value` — attribute with value. The `value` may contain any character except space or `]`.\n\t* `attr=\"value\"` or `attr='value'` — attribute with value in quotes. Quotes are automatically removed. Expression values like `attr={value}` are supported and can be identified by `valueType: \"expression\"` property.\n\t* `attr.` — boolean attribute, e.g. attribute without value, like `required` in `<input>`.\n    * `!attr` – implicit attribute, will be outputted if its value is not empty. Used as a placeholder to preserve attribute order in output.\n\t* `./non/attr/value` — value for default attribute. In other words, anything that doesn’t match a attribute name characters. Can be a single- or double-quotted as well. Default attribute is stored with `null` as name and should be used later, for example, to resolve predefined attributes.\n* `.class` — shorthand for `class` attribute. Note that an element can have multiple classes, like `.class1.class2.class3`.\n* `#id` — shorthand for `id` attribute.\n* `{text}` — node’s text content\n* `*N` — element repeater, tells parser to create `N` copies of given node.\n* `/` — optional self-closing operator. Marks element with `node.selfClosing = true`.\n\n### Operators\n\nEach element of abbreviation must be separated with any of these operators:\n\n```\nelem1+elem2>elem3\n```\n\n* `+` — sibling operator, adds next element as a next sibling of current element in tree.\n* `>` — child operator, adds next element as a child of current element.\n* `^` — climb-up operator, adds next element as a child of current element’s parent node. Multiple climb-up operators are allowed, each operator moves one level up by tree.\n\n### Groups\n\nA set of elements could be grouped using `()`, mostly for repeating and for easier elements nesting:\n\n```\na>(b>c+d)*4+(e+f)\n```\n\nGroups can be optionally concatenated with `+` operator.\n"
  },
  {
    "path": "packages/abbreviation/package.json",
    "content": "{\n  \"name\": \"@emmetio/abbreviation\",\n  \"version\": \"2.3.3\",\n  \"description\": \"Emmet standalone abbreviation parser\",\n  \"main\": \"./dist/index.cjs\",\n  \"module\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"type\": \"module\",\n  \"exports\": {\n    \"import\": \"./dist/index.js\",\n    \"require\": \"./dist/index.cjs\"\n  },\n  \"scripts\": {\n    \"test\": \"tsx --test ./test/*.ts\",\n    \"build\": \"rollup -c\",\n    \"watch\": \"rollup -wc\",\n    \"clean\": \"rm -rf ./dist\",\n    \"prepublishOnly\": \"npm test && npm run clean && npm run build\"\n  },\n  \"keywords\": [\n    \"emmet\",\n    \"abbreviation\"\n  ],\n  \"author\": \"Sergey Chikuyonok <serge.che@gmail.com>\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"@emmetio/scanner\": \"^1.0.4\"\n  },\n  \"directories\": {\n    \"test\": \"test\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/emmetio/emmet.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/emmetio/emmet/issues\"\n  },\n  \"homepage\": \"https://github.com/emmetio/emmet#readme\"\n}\n"
  },
  {
    "path": "packages/abbreviation/rollup.config.js",
    "content": "import typescript from '@rollup/plugin-typescript';\n\nexport default {\n    input: './src/index.ts',\n    external: ['@emmetio/scanner'],\n    plugins: [typescript()],\n    output: [{\n        format: 'cjs',\n        sourcemap: true,\n        exports: 'named',\n        file: './dist/index.cjs'\n    }, {\n        format: 'es',\n        sourcemap: true,\n        file: './dist/index.js'\n    }]\n};\n"
  },
  {
    "path": "packages/abbreviation/src/convert.ts",
    "content": "import { isQuote, isBracket } from './parser';\nimport type { TokenGroup, TokenStatement, TokenElement, TokenAttribute } from './parser';\nimport type { Abbreviation, ParserOptions, AbbreviationNode, ConvertState, Value, AbbreviationAttribute, AttributeType } from './types';\nimport type { Repeater, ValueToken, Quote, Field } from './tokenizer';\nimport stringify from './stringify';\n\nconst urlRegex = /^((https?:|ftp:|file:)?\\/\\/|(www|ftp)\\.)[^ ]*$/;\nconst emailRegex = /^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,5}$/;\n\n/**\n * Converts given token-based abbreviation into simplified and unrolled node-based\n * abbreviation\n */\nexport default function convert(abbr: TokenGroup, options: ParserOptions = {}): Abbreviation {\n    let textInserted = false;\n\n    let cleanText: string | string[] | undefined;\n    if (options.text) {\n        if (Array.isArray(options.text)) {\n            cleanText = options.text.filter(s => s.trim());\n        } else {\n            cleanText = options.text;\n        }\n    }\n\n    const result: Abbreviation = {\n        type: 'Abbreviation',\n        children: convertGroup(abbr, {\n            inserted: false,\n            repeaters: [],\n            text: options.text,\n            cleanText,\n            repeatGuard: options.maxRepeat || Number.POSITIVE_INFINITY,\n            getText(pos) {\n                textInserted = true;\n\n                let value: string;\n                if (Array.isArray(options.text)) {\n                    if (pos !== undefined && pos >= 0 && pos < cleanText!.length) {\n                        return cleanText![pos];\n                    }\n                    value = pos !== undefined ? options.text[pos] : options.text.join('\\n');\n                } else {\n                    value = options.text ?? '';\n                }\n                return value;\n            },\n            getVariable(name) {\n                const varValue = options.variables && options.variables[name];\n                return varValue != null ? varValue : name;\n            }\n        })\n    };\n\n    if (options.text != null && !textInserted) {\n        // Text given but no implicitly repeated elements: insert it into\n        // deepest child\n        const deepest = deepestNode(last(result.children));\n        if (deepest) {\n            const text = Array.isArray(options.text) ? options.text.join('\\n') : options.text;\n            insertText(deepest, text);\n\n            if (deepest.name === 'a' && options.href) {\n                // Automatically update value of `<a>` element if inserting URL or email\n                insertHref(deepest, text);\n            }\n        }\n    }\n\n    return result;\n}\n\n/**\n * Converts given statement to abbreviation nodes\n */\nfunction convertStatement(node: TokenStatement, state: ConvertState): AbbreviationNode[] {\n    let result: AbbreviationNode[] = [];\n\n    if (node.repeat) {\n        // Node is repeated: we should create copies of given node\n        // and supply context token with actual repeater state\n        const original = node.repeat;\n        const repeat = { ...original } as Repeater;\n        repeat.count = repeat.implicit && Array.isArray(state.text)\n            ? state.cleanText!.length\n            : (repeat.count || 1);\n        let items: AbbreviationNode[];\n\n        state.repeaters.push(repeat);\n\n        for (let i = 0; i < repeat.count; i++) {\n            repeat.value = i;\n            node.repeat = repeat;\n            items = isGroup(node)\n                ? convertGroup(node, state)\n                : convertElement(node, state);\n\n            if (repeat.implicit && !state.inserted) {\n                // It’s an implicit repeater but no repeater placeholders found inside,\n                // we should insert text into deepest node\n                const target = last(items);\n                const deepest = target && deepestNode(target);\n                if (deepest) {\n                    insertText(deepest, state.getText(repeat.value));\n                }\n            }\n\n            result = result.concat(items);\n\n            // We should output at least one repeated item even if it’s reached\n            // repeat limit\n            if (--state.repeatGuard <= 0) {\n                break;\n            }\n        }\n\n        state.repeaters.pop();\n        node.repeat = original;\n\n        if (repeat.implicit) {\n            state.inserted = true;\n        }\n    } else {\n        result = result.concat(isGroup(node) ? convertGroup(node, state) : convertElement(node, state));\n    }\n\n    return result;\n}\n\nfunction convertElement(node: TokenElement, state: ConvertState): AbbreviationNode[] {\n    let children: AbbreviationNode[] = [];\n\n    const elem = {\n        type: 'AbbreviationNode',\n        name: node.name && stringifyName(node.name, state),\n        value: node.value && stringifyValue(node.value, state),\n        attributes: void 0,\n        children,\n        repeat: node.repeat && { ...node.repeat },\n        selfClosing: node.selfClose,\n    } as AbbreviationNode;\n    let result: AbbreviationNode[] = [elem];\n\n    for (const child of node.elements) {\n        children = children.concat(convertStatement(child, state));\n    }\n\n    if (node.attributes) {\n        elem.attributes = [];\n        for (const attr of node.attributes) {\n            elem.attributes.push(convertAttribute(attr, state));\n        }\n    }\n\n    // In case if current node is a text-only snippet without fields, we should\n    // put all children as siblings\n    if (!elem.name && !elem.attributes && elem.value && !elem.value.some(isField)) {\n        // XXX it’s unclear that `children` is not bound to `elem`\n        // due to concat operation\n        result = result.concat(children);\n    } else {\n        elem.children = children;\n    }\n\n    return result;\n}\n\nfunction convertGroup(node: TokenGroup, state: ConvertState): AbbreviationNode[] {\n    let result: AbbreviationNode[] = [];\n    for (const child of node.elements) {\n        result = result.concat(convertStatement(child, state));\n    }\n\n    if (node.repeat) {\n        result = attachRepeater(result, node.repeat);\n    }\n\n    return result;\n}\n\nfunction convertAttribute(node: TokenAttribute, state: ConvertState): AbbreviationAttribute {\n    let implied = false;\n    let isBoolean = false;\n    let valueType: AttributeType = node.expression ? 'expression' : 'raw';\n    let value: Value[] | undefined;\n    const name = node.name && stringifyName(node.name, state);\n\n    if (name && name[0] === '!') {\n        implied = true;\n    }\n\n    if (name && name[name.length - 1] === '.') {\n        isBoolean = true;\n    }\n\n    if (node.value) {\n        const tokens = node.value.slice();\n\n        if (isQuote(tokens[0])) {\n            // It’s a quoted value: remove quotes from output but mark attribute\n            // value as quoted\n            const quote = tokens.shift() as Quote;\n            if (tokens.length && last(tokens).type === quote.type) {\n                tokens.pop();\n            }\n            valueType = quote.single ? 'singleQuote' : 'doubleQuote';\n        } else if (isBracket(tokens[0], 'expression', true)) {\n            // Value is expression: remove brackets but mark value type\n            valueType = 'expression';\n            tokens.shift();\n            if (isBracket(last(tokens), 'expression', false)) {\n                tokens.pop();\n            }\n        }\n\n        value = stringifyValue(tokens, state);\n    }\n\n    return {\n        name: isBoolean || implied\n            ? name!.slice(implied ? 1 : 0, isBoolean ? -1 : void 0)\n            : name,\n        value,\n        boolean: isBoolean,\n        implied,\n        valueType,\n        multiple: node.multiple\n    };\n}\n\n/**\n * Converts given token list to string\n */\nfunction stringifyName(tokens: ValueToken[], state: ConvertState): string {\n    let str = '';\n    for (let i = 0; i < tokens.length; i++) {\n        str += stringify(tokens[i], state);\n    }\n\n    return str;\n}\n\n/**\n * Converts given token list to value list\n */\nfunction stringifyValue(tokens: ValueToken[], state: ConvertState): Value[] {\n    const result: Value[] = [];\n    let str = '';\n    for (let i = 0, token: ValueToken; i < tokens.length; i++) {\n        token = tokens[i];\n        if (isField(token)) {\n            // We should keep original fields in output since some editors has their\n            // own syntax for field or doesn’t support fields at all so we should\n            // capture actual field location in output stream\n            if (str) {\n                result.push(str);\n                str = '';\n            }\n            result.push(token);\n        } else {\n            str += stringify(token, state);\n        }\n    }\n\n    if (str) {\n        result.push(str);\n    }\n\n    return result;\n}\n\nexport function isGroup(node: any): node is TokenGroup {\n    return node.type === 'TokenGroup';\n}\n\nfunction isField(token: any): token is Field {\n    return typeof token === 'object' && token.type === 'Field' && token.index != null;\n}\n\nfunction last<T>(arr: T[]): T {\n    return arr[arr.length - 1];\n}\n\nfunction deepestNode(node: AbbreviationNode): AbbreviationNode {\n    return node.children.length ? deepestNode(last(node.children)) : node;\n}\n\nfunction insertText(node: AbbreviationNode, text: string) {\n    if (node.value) {\n        const lastToken = last(node.value);\n        if (typeof lastToken === 'string') {\n            node.value[node.value.length - 1] += text;\n        } else {\n            node.value.push(text);\n        }\n    } else {\n        node.value = [text];\n    }\n}\n\nfunction insertHref(node: AbbreviationNode, text: string) {\n    let href = '';\n    if (urlRegex.test(text)) {\n        href = text;\n        if (!/\\w+:/.test(href) && !href.startsWith('//')) {\n            href = `http://${href}`;\n        }\n    } else if (emailRegex.test(text)) {\n        href = `mailto:${text}`;\n    }\n\n    const hrefAttribute = node.attributes?.find(attr => attr.name === 'href');\n    if (!hrefAttribute) {\n        if (!node.attributes) {\n            node.attributes = [];\n        }\n        node.attributes.push({ name: 'href', value: [href], valueType: 'doubleQuote' });\n    } else if (!hrefAttribute.value) {\n        hrefAttribute.value = [href];\n    }\n}\n\nfunction attachRepeater(items: AbbreviationNode[], repeater: Repeater): AbbreviationNode[] {\n    for (const item of items) {\n        if (!item.repeat) {\n            item.repeat = { ...repeater };\n        }\n    }\n\n    return items;\n}\n"
  },
  {
    "path": "packages/abbreviation/src/index.ts",
    "content": "import { ScannerError } from '@emmetio/scanner';\nimport parse, { type TokenGroup } from './parser';\nimport tokenize, { getToken, type AllTokens } from './tokenizer';\nimport convert from './convert';\nimport type { ParserOptions } from './types';\n\nexport { parse, tokenize, getToken, convert };\nexport * from './tokenizer/tokens';\nexport * from './types';\nexport type MarkupAbbreviation = TokenGroup;\n\n/**\n * Parses given abbreviation into node tree\n */\nexport default function parseAbbreviation(abbr: string | AllTokens[], options?: ParserOptions) {\n    try {\n        const tokens = typeof abbr === 'string' ? tokenize(abbr) : abbr;\n        return convert(parse(tokens, options), options);\n    } catch (err) {\n        if (err instanceof ScannerError && typeof abbr === 'string') {\n            err.message += `\\n${abbr}\\n${'-'.repeat(err.pos)}^`;\n        }\n\n        throw err;\n    }\n}\n"
  },
  {
    "path": "packages/abbreviation/src/parser/TokenScanner.ts",
    "content": "import type { AllTokens } from '../tokenizer';\n\nexport interface TokenScanner {\n    tokens: AllTokens[];\n    start: number;\n    pos: number;\n    size: number;\n}\n\ntype TestFn = (token?: AllTokens) => boolean;\n\nexport default function tokenScanner(tokens: AllTokens[]): TokenScanner {\n    return {\n        tokens,\n        start: 0,\n        pos: 0,\n        size: tokens.length\n    };\n}\n\nexport function peek(scanner: TokenScanner): AllTokens | undefined {\n    return scanner.tokens[scanner.pos];\n}\n\nexport function next(scanner: TokenScanner): AllTokens | undefined {\n    return scanner.tokens[scanner.pos++];\n}\n\nexport function slice(scanner: TokenScanner, from = scanner.start, to = scanner.pos): AllTokens[] {\n    return scanner.tokens.slice(from, to);\n}\n\nexport function readable(scanner: TokenScanner): boolean {\n    return scanner.pos < scanner.size;\n}\n\nexport function consume(scanner: TokenScanner, test: TestFn): boolean {\n    const token = peek(scanner);\n    if (token && test(token)) {\n        scanner.pos++;\n        return true;\n    }\n\n    return false;\n}\n\nexport function error(scanner: TokenScanner, message: string, token = peek(scanner)) {\n    if (token && token.start != null) {\n        message += ` at ${token.start}`;\n    }\n\n    const err = new Error(message);\n    err['pos'] = token && token.start;\n\n    return err;\n}\n\nexport function consumeWhile(scanner: TokenScanner, test: TestFn): boolean {\n    const start = scanner.pos;\n    while (consume(scanner, test)) { /* */ }\n    return scanner.pos !== start;\n}\n"
  },
  {
    "path": "packages/abbreviation/src/parser/index.ts",
    "content": "import type { NameToken, ValueToken, Repeater, AllTokens, BracketType, Bracket, Operator, OperatorType, Quote, WhiteSpace, Literal } from '../tokenizer';\nimport tokenScanner, { type TokenScanner, peek, consume, readable, next, error, slice } from './TokenScanner';\nimport type { ParserOptions } from '../types';\n\nexport type TokenStatement = TokenElement | TokenGroup;\n\nexport interface TokenAttribute {\n    name?: ValueToken[];\n    value?: ValueToken[];\n    expression?: boolean;\n    /**\n     * Indicates that current attribute was repeated multiple times in a row.\n     * Used to alter output of multiple shorthand attributes like `..` (double class)\n     */\n    multiple?: boolean;\n}\n\nexport interface TokenElement {\n    type: 'TokenElement';\n    name?: NameToken[];\n    attributes?: TokenAttribute[];\n    value?: ValueToken[];\n    repeat?: Repeater;\n    selfClose: boolean;\n    elements: TokenStatement[];\n}\n\nexport interface TokenGroup {\n    type: 'TokenGroup';\n    elements: TokenStatement[];\n    repeat?: Repeater;\n}\n\nexport default function abbreviation(abbr: AllTokens[], options: ParserOptions = {}): TokenGroup {\n    const scanner = tokenScanner(abbr);\n    const result = statements(scanner, options);\n    if (readable(scanner)) {\n        throw error(scanner, 'Unexpected character');\n    }\n\n    return result;\n}\n\nfunction statements(scanner: TokenScanner, options: ParserOptions): TokenGroup {\n    const result: TokenGroup = {\n        type: 'TokenGroup',\n        elements: []\n    };\n\n    let ctx: TokenStatement = result;\n    let node: TokenStatement | undefined;\n    const stack: TokenStatement[] = [];\n\n    while (readable(scanner)) {\n        if (node = element(scanner, options) || group(scanner, options)) {\n            ctx.elements.push(node);\n            if (consume(scanner, isChildOperator)) {\n                stack.push(ctx);\n                ctx = node;\n            } else if (consume(scanner, isSiblingOperator)) {\n                continue;\n            } else if (consume(scanner, isClimbOperator)) {\n                do {\n                    if (stack.length) {\n                        ctx = stack.pop()!;\n                    }\n                } while (consume(scanner, isClimbOperator));\n            }\n        } else {\n            break;\n        }\n    }\n\n    return result;\n}\n\n/**\n * Consumes group from given scanner\n */\nfunction group(scanner: TokenScanner, options: ParserOptions): TokenGroup | undefined {\n    if (consume(scanner, isGroupStart)) {\n        const result = statements(scanner, options);\n        const token = next(scanner);\n        if (isBracket(token, 'group', false)) {\n            result.repeat = repeater(scanner);\n        }\n        return result;\n    }\n}\n\n/**\n * Consumes single element from given scanner\n */\nfunction element(scanner: TokenScanner, options: ParserOptions): TokenElement | undefined {\n    let attr: TokenAttribute | TokenAttribute[] | undefined;\n    const elem: TokenElement = {\n        type: 'TokenElement',\n        name: void 0,\n        attributes: void 0,\n        value: void 0,\n        repeat: void 0,\n        selfClose: false,\n        elements: []\n    };\n\n    if (elementName(scanner, options)) {\n        elem.name = slice(scanner) as NameToken[];\n    }\n\n    while (readable(scanner)) {\n        scanner.start = scanner.pos;\n        if (!elem.repeat && !isEmpty(elem) && consume(scanner, isRepeater)) {\n            elem.repeat = scanner.tokens[scanner.pos - 1] as Repeater;\n        } else if (!elem.value && text(scanner)) {\n            elem.value = getText(scanner);\n        } else if (attr = shortAttribute(scanner, 'id', options) || shortAttribute(scanner, 'class', options) || attributeSet(scanner)) {\n            if (!elem.attributes) {\n                elem.attributes = Array.isArray(attr) ? attr.slice() : [attr];\n            } else {\n                elem.attributes = elem.attributes.concat(attr);\n            }\n        } else {\n            if (!isEmpty(elem) && consume(scanner, isCloseOperator)) {\n                elem.selfClose = true;\n                if (!elem.repeat && consume(scanner, isRepeater)) {\n                    elem.repeat = scanner.tokens[scanner.pos - 1] as Repeater;\n                }\n            }\n            break;\n        }\n    }\n\n    return !isEmpty(elem) ? elem : void 0;\n}\n\n/**\n * Consumes attribute set from given scanner\n */\nfunction attributeSet(scanner: TokenScanner): TokenAttribute[] | undefined {\n    if (consume(scanner, isAttributeSetStart)) {\n        const attributes: TokenAttribute[] = [];\n        let attr: TokenAttribute | undefined;\n\n        while (readable(scanner)) {\n            if (attr = attribute(scanner)) {\n                attributes.push(attr);\n            } else if (consume(scanner, isAttributeSetEnd)) {\n                break;\n            } else if (!consume(scanner, isWhiteSpace)) {\n                throw error(scanner, `Unexpected \"${peek(scanner)!.type}\" token`);\n            }\n        }\n\n        return attributes;\n    }\n}\n\n/**\n * Consumes attribute shorthand (class or id) from given scanner\n */\nfunction shortAttribute(scanner: TokenScanner, type: 'class' | 'id', options: ParserOptions): TokenAttribute | undefined {\n    if (isOperator(peek(scanner), type)) {\n        scanner.pos++;\n\n        // Consume multiple operators\n        let count = 1;\n        while (isOperator(peek(scanner), type)) {\n            scanner.pos++;\n            count++;\n        }\n\n        const attr: TokenAttribute = {\n            name: [createLiteral(type)]\n        };\n\n        if (count > 1) {\n            attr.multiple = true;\n        }\n\n        // Consume expression after shorthand start for React-like components\n        if (options.jsx && text(scanner)) {\n            attr.value = getText(scanner);\n            attr.expression = true;\n        } else if (quoted(scanner)) {\n            attr.value = slice(scanner, scanner.start + 1, scanner.pos - 1) as ValueToken[];\n        } else {\n            attr.value = literal(scanner) ? slice(scanner) as ValueToken[] : void 0;\n        }\n\n        return attr;\n    }\n}\n\n/**\n * Consumes single attribute from given scanner\n */\nfunction attribute(scanner: TokenScanner): TokenAttribute | undefined {\n    if (quoted(scanner)) {\n        // Consumed quoted value: it’s a value for default attribute\n        return {\n            value: slice(scanner) as ValueToken[]\n        };\n    }\n\n    if (literal(scanner, true)) {\n        const name = slice(scanner) as NameToken[];\n        let value: ValueToken[] | undefined;\n        if (consume(scanner, isEquals)) {\n            if (quoted(scanner) || literal(scanner, true)) {\n                value = slice(scanner) as ValueToken[];\n            }\n        }\n\n        return { name, value };\n    }\n}\n\nfunction repeater(scanner: TokenScanner): Repeater | undefined {\n    return isRepeater(peek(scanner))\n        ? scanner.tokens[scanner.pos++] as Repeater\n        : void 0;\n}\n\n/**\n * Consumes quoted value from given scanner, if possible\n */\nfunction quoted(scanner: TokenScanner): boolean {\n    const start = scanner.pos;\n    const quote = peek(scanner);\n    if (isQuote(quote)) {\n        scanner.pos++;\n        while (readable(scanner)) {\n            if (isQuote(next(scanner), quote.single)) {\n                scanner.start = start;\n                return true;\n            }\n        }\n\n        throw error(scanner, 'Unclosed quote', quote);\n    }\n\n    return false;\n}\n\n/**\n * Consumes literal (unquoted value) from given scanner\n */\nfunction literal(scanner: TokenScanner, allowBrackets?: boolean): boolean {\n    const start = scanner.pos;\n    const brackets: { [type in BracketType]: number } = {\n        attribute: 0,\n        expression: 0,\n        group: 0\n    };\n\n    while (readable(scanner)) {\n        const token = peek(scanner);\n        if (brackets.expression) {\n            // If we’re inside expression, we should consume all content in it\n            if (isBracket(token, 'expression')) {\n                brackets[token.context] += token.open ? 1 : -1;\n            }\n        } else if (isQuote(token) || isOperator(token) || isWhiteSpace(token) || isRepeater(token)) {\n            break;\n        } else if (isBracket(token)) {\n            if (!allowBrackets) {\n                break;\n            }\n\n            if (token.open) {\n                brackets[token.context]++;\n            } else if (!brackets[token.context]) {\n                // Stop if found unmatched closing brace: it must be handled\n                // by parent consumer\n                break;\n            } else {\n                brackets[token.context]--;\n            }\n        }\n\n        scanner.pos++;\n    }\n\n    if (start !== scanner.pos) {\n        scanner.start = start;\n        return true;\n    }\n\n    return false;\n}\n\n/**\n * Consumes element name from given scanner\n */\nfunction elementName(scanner: TokenScanner, options: ParserOptions): boolean {\n    const start = scanner.pos;\n\n    if (options.jsx && consume(scanner, isCapitalizedLiteral)) {\n        // Check for edge case: consume immediate capitalized class names\n        // for React-like components, e.g. `Foo.Bar.Baz`\n        while (readable(scanner)) {\n            const { pos } = scanner;\n            if (!consume(scanner, isClassNameOperator) || !consume(scanner, isCapitalizedLiteral)) {\n                scanner.pos = pos;\n                break;\n            }\n        }\n    }\n\n    while (readable(scanner) && consume(scanner, isElementName)) {\n        // empty\n    }\n\n    if (scanner.pos !== start) {\n        scanner.start = start;\n        return true;\n    }\n\n    return false;\n}\n\n/**\n * Consumes text value from given scanner\n */\nfunction text(scanner: TokenScanner): boolean {\n    const start = scanner.pos;\n    if (consume(scanner, isTextStart)) {\n        let brackets = 0;\n        while (readable(scanner)) {\n            const token = next(scanner);\n            if (isBracket(token, 'expression')) {\n                if (token.open) {\n                    brackets++;\n                } else if (!brackets) {\n                    break;\n                } else {\n                    brackets--;\n                }\n            }\n        }\n\n        scanner.start = start;\n        return true;\n    }\n\n    return false;\n}\n\nfunction getText(scanner: TokenScanner): ValueToken[] {\n    let from = scanner.start;\n    let to = scanner.pos;\n    if (isBracket(scanner.tokens[from], 'expression', true)) {\n        from++;\n    }\n\n    if (isBracket(scanner.tokens[to - 1], 'expression', false)) {\n        to--;\n    }\n\n    return slice(scanner, from, to) as ValueToken[];\n}\n\nexport function isBracket(token: AllTokens | undefined, context?: BracketType, isOpen?: boolean): token is Bracket {\n    return Boolean(token && token.type === 'Bracket'\n        && (!context || token.context === context)\n        && (isOpen == null || token.open === isOpen));\n}\n\nexport function isOperator(token: AllTokens | undefined, type?: OperatorType): token is Operator {\n    return Boolean(token && token.type === 'Operator' && (!type || token.operator === type));\n}\n\nexport function isQuote(token: AllTokens | undefined, isSingle?: boolean): token is Quote {\n    return Boolean(token && token.type === 'Quote' && (isSingle == null || token.single === isSingle));\n}\n\nfunction isWhiteSpace(token?: AllTokens): token is WhiteSpace {\n    return Boolean(token && token.type === 'WhiteSpace');\n}\n\nfunction isEquals(token: AllTokens) {\n    return isOperator(token, 'equal');\n}\n\nfunction isRepeater(token?: AllTokens): token is Repeater {\n    return Boolean(token && token.type === 'Repeater');\n}\n\nfunction isLiteral(token: AllTokens): token is Literal {\n    return token.type === 'Literal';\n}\n\nfunction isCapitalizedLiteral(token: AllTokens) {\n    if (isLiteral(token)) {\n        const ch = token.value.charCodeAt(0);\n        return ch >= 65 && ch <= 90;\n    }\n    return false;\n}\n\nfunction isElementName(token: AllTokens): boolean {\n    return token.type === 'Literal' || token.type === 'RepeaterNumber' || token.type === 'RepeaterPlaceholder';\n}\n\nfunction isClassNameOperator(token: AllTokens) {\n    return isOperator(token, 'class');\n}\n\nfunction isAttributeSetStart(token?: AllTokens) {\n    return isBracket(token, 'attribute', true);\n}\n\nfunction isAttributeSetEnd(token?: AllTokens) {\n    return isBracket(token, 'attribute', false);\n}\n\nfunction isTextStart(token: AllTokens) {\n    return isBracket(token, 'expression', true);\n}\n\nfunction isGroupStart(token: AllTokens) {\n    return isBracket(token, 'group', true);\n}\n\nfunction createLiteral(value: string): Literal {\n    return { type: 'Literal', value };\n}\n\nfunction isEmpty(elem: TokenElement): boolean {\n    return !elem.name && !elem.value && !elem.attributes;\n}\n\nfunction isChildOperator(token: AllTokens) {\n    return isOperator(token, 'child');\n}\n\nfunction isSiblingOperator(token: AllTokens) {\n    return isOperator(token, 'sibling');\n}\n\nfunction isClimbOperator(token: AllTokens) {\n    return isOperator(token, 'climb');\n}\n\nfunction isCloseOperator(token: AllTokens) {\n    return isOperator(token, 'close');\n}\n"
  },
  {
    "path": "packages/abbreviation/src/stringify.ts",
    "content": "import type { Token, Literal, Bracket, Field, RepeaterPlaceholder, Repeater, RepeaterNumber, ValueToken, Quote, Operator, OperatorType, WhiteSpace } from './tokenizer/tokens';\nimport type { ConvertState } from './types';\n\ntype TokenVisitor = (token: Token, state: ConvertState) => string;\n\nconst operators: { [key in OperatorType]: string } = {\n    child: '>',\n    class: '.',\n    climb: '^',\n    id: '#',\n    equal: '=',\n    close: '/',\n    sibling: '+'\n};\n\nconst tokenVisitor: { [name: string]: TokenVisitor } = {\n    Literal(token: Literal): string {\n        return token.value;\n    },\n    Quote(token: Quote) {\n        return token.single ? '\\'' : '\"';\n    },\n    Bracket(token: Bracket): string {\n        if (token.context === 'attribute') {\n            return token.open ? '[' : ']';\n        } else if (token.context === 'expression') {\n            return token.open ? '{' : '}';\n        } else {\n            return token.open ? '(' : '}';\n        }\n    },\n    Operator(token: Operator) {\n        return operators[token.operator];\n    },\n    Field(token: Field, state) {\n        if (token.index != null) {\n            // It’s a field: by default, return TextMate-compatible field\n            return token.name\n                ? `\\${${token.index}:${token.name}}`\n                : `\\${${token.index}`;\n        } else if (token.name) {\n            // It’s a variable\n            return state.getVariable(token.name);\n        }\n\n        return '';\n    },\n    RepeaterPlaceholder(token: RepeaterPlaceholder, state) {\n        // Find closest implicit repeater\n        let repeater: Repeater | undefined;\n        for (let i = state.repeaters.length - 1; i >= 0; i--) {\n            if (state.repeaters[i]!.implicit) {\n                repeater = state.repeaters[i]!;\n                break;\n            }\n        }\n\n        state.inserted = true;\n        return state.getText(repeater && repeater.value);\n    },\n    RepeaterNumber(token: RepeaterNumber, state) {\n        let value = 1;\n        const lastIx = state.repeaters.length - 1;\n        // const repeaterIx = Math.max(0, state.repeaters.length - 1 - token.parent);\n        const repeater = state.repeaters[lastIx];\n        if (repeater) {\n            value = token.reverse\n                ? token.base + repeater.count - repeater.value! - 1\n                : token.base + repeater.value!;\n\n            if (token.parent) {\n                const parentIx = Math.max(0, lastIx - token.parent);\n                if (parentIx !== lastIx) {\n                    const parentRepeater = state.repeaters[parentIx];\n                    value += repeater.count * parentRepeater.value;\n                }\n            }\n        }\n\n        let result = String(value);\n        while (result.length < token.size) {\n            result = '0' + result;\n        }\n\n        return result;\n    },\n    WhiteSpace(token: WhiteSpace) {\n        return token.value;\n    }\n};\n\n/**\n * Converts given value token to string\n */\nexport default function stringify(token: ValueToken, state: ConvertState): string {\n    if (!tokenVisitor[token.type]) {\n        throw new Error(`Unknown token ${token.type}`);\n    }\n    return tokenVisitor[token.type](token, state);\n}\n"
  },
  {
    "path": "packages/abbreviation/src/tokenizer/index.ts",
    "content": "import Scanner, { isSpace, isQuote, isNumber, isAlpha, isAlphaNumericWord, isUmlaut } from '@emmetio/scanner';\nimport type { Literal, WhiteSpace, Quote, Bracket, BracketType, OperatorType, Operator, RepeaterPlaceholder, Repeater, Field, RepeaterNumber, AllTokens } from './tokens';\nimport { Chars, escaped } from './utils';\n\nexport * from './tokens';\n\ntype Context =  { [ctx in BracketType]: number } & { quote: number };\n\nexport default function tokenize(source: string): AllTokens[] {\n    const scanner = new Scanner(source);\n    const result: AllTokens[] = [];\n    const ctx: Context = {\n        group: 0,\n        attribute: 0,\n        expression: 0,\n        quote: 0\n    };\n\n    let ch = 0;\n    let token: AllTokens | undefined;\n\n    while (!scanner.eof()) {\n        ch = scanner.peek();\n        token = getToken(scanner, ctx);\n\n        if (token) {\n            result.push(token);\n            if (token.type === 'Quote') {\n                ctx.quote = ch === ctx.quote ? 0 : ch;\n            } else if (token.type === 'Bracket') {\n                ctx[token.context] += token.open ? 1 : -1;\n            }\n        } else {\n            throw scanner.error('Unexpected character');\n        }\n    }\n\n    return result;\n}\n\n/**\n * Returns next token from given scanner, if possible\n */\nexport function getToken(scanner: Scanner, ctx: Context): AllTokens | undefined {\n    return field(scanner, ctx)\n        || repeaterPlaceholder(scanner)\n        || repeaterNumber(scanner)\n        || repeater(scanner)\n        || whiteSpace(scanner)\n        || literal(scanner, ctx)\n        || operator(scanner)\n        || quote(scanner)\n        || bracket(scanner);\n}\n\n/**\n * Consumes literal from given scanner\n */\nfunction literal(scanner: Scanner, ctx: Context): Literal | undefined {\n    const start = scanner.pos;\n    const expressionStart = ctx.expression;\n    let value = '';\n\n    while (!scanner.eof()) {\n        // Consume escaped sequence no matter of context\n        if (escaped(scanner)) {\n            value += scanner.current();\n            continue;\n        }\n\n        const ch = scanner.peek();\n\n        if (ch === Chars.Slash && !ctx.quote && !ctx.expression && !ctx.attribute) {\n            // Special case for `/` character between numbers in class names\n            const prev = scanner.string.charCodeAt(scanner.pos - 1);\n            const next = scanner.string.charCodeAt(scanner.pos + 1);\n            if (isNumber(prev) && isNumber(next)) {\n                value += scanner.string[scanner.pos++];\n                continue;\n            }\n        }\n\n        if (ch === ctx.quote || ch === Chars.Dollar || isAllowedOperator(ch, ctx)) {\n            // 1. Found matching quote\n            // 2. The `$` character has special meaning in every context\n            // 3. Depending on context, some characters should be treated as operators\n            break;\n        }\n\n        if (expressionStart) {\n            // Consume nested expressions, e.g. span{{foo}}\n            if (ch === Chars.CurlyBracketOpen) {\n                ctx.expression++;\n            } else if (ch === Chars.CurlyBracketClose) {\n                if (ctx.expression > expressionStart) {\n                    ctx.expression--;\n                } else {\n                    break;\n                }\n            }\n        } else if (!ctx.quote) {\n            // Consuming element name\n            if (!ctx.attribute && !isElementName(ch)) {\n                break;\n            }\n\n            if (isAllowedSpace(ch, ctx) || isAllowedRepeater(ch, ctx) || isQuote(ch) || bracketType(ch)) {\n                // Stop for characters not allowed in unquoted literal\n                break;\n            }\n        }\n\n        value += scanner.string[scanner.pos++];\n    }\n\n    if (start !== scanner.pos) {\n        scanner.start = start;\n        return {\n            type: 'Literal',\n            value,\n            start,\n            end: scanner.pos\n        };\n    }\n}\n\n/**\n * Consumes white space characters as string literal from given scanner\n */\nfunction whiteSpace(scanner: Scanner): WhiteSpace | undefined {\n    const start = scanner.pos;\n    if (scanner.eatWhile(isSpace)) {\n        return {\n            type: 'WhiteSpace',\n            start,\n            end: scanner.pos,\n            value: scanner.substring(start, scanner.pos)\n        };\n    }\n}\n\n/**\n * Consumes quote from given scanner\n */\nfunction quote(scanner: Scanner): Quote | undefined {\n    const ch = scanner.peek();\n    if (isQuote(ch)) {\n        return {\n            type: 'Quote',\n            single: ch === Chars.SingleQuote,\n            start: scanner.pos++,\n            end: scanner.pos\n        };\n    }\n}\n\n/**\n * Consumes bracket from given scanner\n */\nfunction bracket(scanner: Scanner): Bracket | undefined {\n    const ch = scanner.peek();\n    const context = bracketType(ch);\n    if (context) {\n        return {\n            type: 'Bracket',\n            open: isOpenBracket(ch),\n            context,\n            start: scanner.pos++,\n            end: scanner.pos\n        };\n    }\n}\n\n/**\n * Consumes operator from given scanner\n */\nfunction operator(scanner: Scanner): Operator | undefined {\n    const op = operatorType(scanner.peek());\n    if (op) {\n        return {\n            type: 'Operator',\n            operator: op,\n            start: scanner.pos++,\n            end: scanner.pos\n        };\n    }\n}\n\n/**\n * Consumes node repeat token from current stream position and returns its\n * parsed value\n */\nfunction repeater(scanner: Scanner): Repeater | undefined {\n    const start = scanner.pos;\n    if (scanner.eat(Chars.Asterisk)) {\n        scanner.start = scanner.pos;\n        let count = 1;\n        let implicit = false;\n\n        if (scanner.eatWhile(isNumber)) {\n            count = Number(scanner.current());\n        } else {\n            implicit = true;\n        }\n\n        return {\n            type: 'Repeater',\n            count,\n            value: 0,\n            implicit,\n            start,\n            end: scanner.pos\n        };\n    }\n}\n\n/**\n * Consumes repeater placeholder `$#` from given scanner\n */\nfunction repeaterPlaceholder(scanner: Scanner): RepeaterPlaceholder | undefined {\n    const start = scanner.pos;\n    if (scanner.eat(Chars.Dollar) && scanner.eat(Chars.Hash)) {\n        return {\n            type: 'RepeaterPlaceholder',\n            value: void 0,\n            start,\n            end: scanner.pos\n        };\n    }\n\n    scanner.pos = start;\n}\n\n/**\n * Consumes numbering token like `$` from given scanner state\n */\nfunction repeaterNumber(scanner: Scanner): RepeaterNumber | undefined {\n    const start = scanner.pos;\n    if (scanner.eatWhile(Chars.Dollar)) {\n        const size = scanner.pos - start;\n        let reverse = false;\n        let base = 1;\n        let parent = 0;\n\n        if (scanner.eat(Chars.At)) {\n            // Consume numbering modifiers\n            while (scanner.eat(Chars.Climb)) {\n                parent++;\n            }\n\n            reverse = scanner.eat(Chars.Dash);\n            scanner.start = scanner.pos;\n            if (scanner.eatWhile(isNumber)) {\n                base = Number(scanner.current());\n            }\n        }\n\n        scanner.start = start;\n\n        return {\n            type: 'RepeaterNumber',\n            size,\n            reverse,\n            base,\n            parent,\n            start,\n            end: scanner.pos\n        };\n    }\n}\n\nfunction field(scanner: Scanner, ctx: Context): Field | undefined {\n    const start = scanner.pos;\n    // Fields are allowed inside expressions and attributes\n    if ((ctx.expression || ctx.attribute) && scanner.eat(Chars.Dollar) && scanner.eat(Chars.CurlyBracketOpen)) {\n        scanner.start = scanner.pos;\n\n        let index: number | undefined;\n        let name: string = '';\n\n        if (scanner.eatWhile(isNumber)) {\n            // It’s a field\n            index = Number(scanner.current());\n            name = scanner.eat(Chars.Colon) ? consumePlaceholder(scanner) : '';\n        } else if (isAlpha(scanner.peek())) {\n            // It’s a variable\n            name = consumePlaceholder(scanner);\n        }\n\n        if (scanner.eat(Chars.CurlyBracketClose)) {\n            return {\n                type: 'Field',\n                index, name,\n                start,\n                end: scanner.pos\n            };\n        }\n\n        throw scanner.error('Expecting }');\n    }\n\n    // If we reached here then there’s no valid field here, revert\n    // back to starting position\n    scanner.pos = start;\n}\n\n/**\n * Consumes a placeholder: value right after `:` in field. Could be empty\n */\nfunction consumePlaceholder(stream: Scanner): string {\n    const stack: number[] = [];\n    stream.start = stream.pos;\n\n    while (!stream.eof()) {\n        if (stream.eat(Chars.CurlyBracketOpen)) {\n            stack.push(stream.pos);\n        } else if (stream.eat(Chars.CurlyBracketClose)) {\n            if (!stack.length) {\n                stream.pos--;\n                break;\n            }\n            stack.pop();\n        } else {\n            stream.pos++;\n        }\n    }\n\n    if (stack.length) {\n        stream.pos = stack.pop()!;\n        throw stream.error(`Expecting }`);\n    }\n\n    return stream.current();\n}\n\n/**\n * Check if given character code is an operator and it’s allowed in current context\n */\nfunction isAllowedOperator(ch: number, ctx: Context): boolean {\n    const op = operatorType(ch);\n    if (!op || ctx.quote || ctx.expression) {\n        // No operators inside quoted values or expressions\n        return false;\n    }\n\n    // Inside attributes, only `equals` is allowed\n    return !ctx.attribute || op === 'equal';\n}\n\n/**\n * Check if given character is a space character and is allowed to be consumed\n * as a space token in current context\n */\nfunction isAllowedSpace(ch: number, ctx: Context): boolean {\n    return isSpace(ch) && !ctx.expression;\n}\n\n/**\n * Check if given character can be consumed as repeater in current context\n */\nfunction isAllowedRepeater(ch: number, ctx: Context): boolean {\n    return ch === Chars.Asterisk && !ctx.attribute && !ctx.expression;\n}\n\n/**\n * If given character is a bracket, returns it’s type\n */\nfunction bracketType(ch: number): BracketType | undefined {\n    if (ch === Chars.RoundBracketOpen || ch === Chars.RoundBracketClose) {\n        return 'group';\n    }\n\n    if (ch === Chars.SquareBracketOpen || ch === Chars.SquareBracketClose) {\n        return 'attribute';\n    }\n\n    if (ch === Chars.CurlyBracketOpen || ch === Chars.CurlyBracketClose) {\n        return 'expression';\n    }\n}\n\n/**\n * If given character is an operator, returns it’s type\n */\nfunction operatorType(ch: number): OperatorType | undefined {\n    return (ch === Chars.Child && 'child')\n        || (ch === Chars.Sibling && 'sibling')\n        || (ch === Chars.Climb && 'climb')\n        || (ch === Chars.Dot && 'class')\n        || (ch === Chars.Hash && 'id')\n        || (ch === Chars.Slash && 'close')\n        || (ch === Chars.Equals && 'equal')\n        || void 0;\n}\n\n/**\n * Check if given character is an open bracket\n */\nfunction isOpenBracket(ch: number): boolean {\n    return ch === Chars.CurlyBracketOpen\n        || ch === Chars.SquareBracketOpen\n        || ch === Chars.RoundBracketOpen;\n}\n\n/**\n * Check if given character is allowed in element name\n */\nfunction isElementName(ch: number) {\n    return isAlphaNumericWord(ch)\n        || isUmlaut(ch)\n        || ch === Chars.Dash\n        || ch === Chars.Colon\n        || ch === Chars.Excl;\n}\n"
  },
  {
    "path": "packages/abbreviation/src/tokenizer/tokens.ts",
    "content": "export type OperatorType = 'child' | 'sibling' | 'climb' | 'class' | 'id' | 'close' | 'equal';\nexport type BracketType = 'group' | 'attribute' | 'expression';\n\nexport type AllTokens =\n    Bracket | Field | Literal | Operator | Quote | Repeater | RepeaterNumber |\n    RepeaterPlaceholder | WhiteSpace;\n\nexport type NameToken = Literal | RepeaterNumber;\nexport type ValueToken = Literal | Quote | Bracket | Field | RepeaterPlaceholder | RepeaterNumber;\n\nexport interface Token {\n    type: string;\n\n    /** Location of token start in source */\n    start?: number;\n\n    /** Location of token end in source */\n    end?: number;\n}\n\nexport interface Repeater extends Token {\n    type: 'Repeater';\n\n    /** How many times context element should be repeated */\n    count: number;\n\n    /** Position of context element in its repeating sequence */\n    value: number;\n\n    /** Repeater is implicit, e.g. repeated by the amount of text lines selected by user */\n    implicit: boolean;\n}\n\nexport interface RepeaterNumber extends Token {\n    type: 'RepeaterNumber';\n\n    /** Size of repeater content, e.g. the amount consequent numbering characters */\n    size: number;\n\n    /** Should output numbering in reverse order? */\n    reverse: boolean;\n\n    /** Base value to start numbering from */\n    base: number;\n\n    /** Parent offset from which numbering should be used */\n    parent: number;\n}\n\nexport interface RepeaterPlaceholder extends Token {\n    type: 'RepeaterPlaceholder';\n\n    /** Value to insert instead of placeholder */\n    value?: string;\n}\n\nexport interface Field extends Token {\n    type: 'Field';\n    index?: number;\n    name: string;\n}\n\nexport interface Operator extends Token {\n    type: 'Operator';\n    operator: OperatorType;\n}\n\nexport interface Bracket extends Token {\n    type: 'Bracket';\n    open: boolean;\n    context: BracketType;\n}\n\nexport interface Quote extends Token {\n    type: 'Quote';\n    single: boolean;\n}\n\nexport interface Literal extends Token {\n    type: 'Literal';\n    value: string;\n}\n\nexport interface WhiteSpace extends Token {\n    type: 'WhiteSpace';\n    value: string;\n}\n"
  },
  {
    "path": "packages/abbreviation/src/tokenizer/utils.ts",
    "content": "import type Scanner from '@emmetio/scanner';\n\nexport const enum Chars {\n    /** `{` character */\n    CurlyBracketOpen = 123,\n\n    /** `}` character */\n    CurlyBracketClose = 125,\n\n    /** `\\\\` character */\n    Escape = 92,\n\n    /** `=` character */\n    Equals = 61,\n\n    /** `[` character */\n    SquareBracketOpen = 91,\n\n    /** `]` character */\n    SquareBracketClose = 93,\n\n    /** `*` character */\n    Asterisk = 42,\n\n    /** `#` character */\n    Hash = 35,\n\n    /** `$` character */\n    Dollar = 36,\n\n    /** `-` character */\n    Dash = 45,\n\n    /** `.` character */\n    Dot = 46,\n\n    /** `/` character */\n    Slash = 47,\n\n    /** `:` character */\n    Colon = 58,\n\n    /** `!` character */\n    Excl = 33,\n\n    /** `@` character */\n    At = 64,\n\n    /** `_` character */\n    Underscore = 95,\n\n    /** `(` character */\n    RoundBracketOpen = 40,\n\n    /** `)` character */\n    RoundBracketClose = 41,\n\n    /** `+` character */\n    Sibling = 43,\n\n    /** `>` character */\n    Child = 62,\n\n    /** `^` character */\n    Climb = 94,\n\n    /** `'` character */\n    SingleQuote = 39,\n\n    /** `\"\"` character */\n    DoubleQuote = 34,\n}\n\n/**\n * If consumes escape character, sets current stream range to escaped value\n */\nexport function escaped(scanner: Scanner): boolean {\n    if (scanner.eat(Chars.Escape)) {\n        scanner.start = scanner.pos;\n        if (!scanner.eof()) {\n            scanner.pos++;\n        }\n        return true;\n    }\n\n    return false;\n}\n"
  },
  {
    "path": "packages/abbreviation/src/types.ts",
    "content": "import type { Field, Repeater } from './tokenizer';\n\nexport interface ParserOptions {\n    /** Text strings to insert into implicitly repeated elements */\n    text?: string | string[];\n\n    /** Variable values for `${var}` tokens */\n    variables?: { [name: string]: string };\n\n    /** Max amount of repeated elements in abbreviation */\n    maxRepeat?: number;\n\n    /** Enabled JSX parsing mode */\n    jsx?: boolean;\n\n    /** Enable inserting text into href attribute of links */\n    href?: boolean;\n}\n\nexport interface ConvertState {\n    inserted: boolean;\n    text?: string | string[];\n    cleanText?: string | string[];\n    repeatGuard: number;\n\n    /** Context repeaters, e.g. all actual repeaters from parent */\n    repeaters: Repeater[];\n\n    getText(pos?: number): string;\n    getVariable(name: string): string;\n}\n\nexport type Value = string | Field;\nexport type AttributeType = 'raw' | 'singleQuote' | 'doubleQuote' | 'expression';\n\nexport interface Abbreviation {\n    type: 'Abbreviation';\n    children: AbbreviationNode[];\n}\n\nexport interface AbbreviationNode {\n    type: 'AbbreviationNode';\n    name?: string;\n    value?: Value[];\n    repeat?: Repeater;\n    attributes?: AbbreviationAttribute[];\n    children: AbbreviationNode[];\n\n    /**  Indicates current element is self-closing, e.g. should not contain closing pair */\n    selfClosing?: boolean;\n}\n\nexport interface AbbreviationAttribute {\n    name?: string;\n    value?: Value[];\n\n    /** Indicates type of value stored in `.value` property */\n    valueType: AttributeType;\n\n    /** Attribute is boolean (e.g.name equals value) */\n    boolean?: boolean;\n\n    /** Attribute is implied (e.g.must be outputted only if contains non-null value) */\n    implied?: boolean;\n\n    /**\n     * Internal property that indicates that given attribute was specified\n     * more than once as a shorthand. E.g. `..` is a multiple `class` attribute\n     */\n    multiple?: boolean;\n}\n"
  },
  {
    "path": "packages/abbreviation/test/assets/stringify-node.ts",
    "content": "import type { Abbreviation, AbbreviationNode, Value, AbbreviationAttribute } from '../../src';\n\nexport default function stringify(abbr: Abbreviation): string {\n    return abbr.children.map(elem).join('');\n}\n\nfunction elem(node: AbbreviationNode): string {\n    const name = node.name || '?';\n    const attributes = node.attributes\n        ? node.attributes.map(attr => ' ' + attribute(attr))\n        : '';\n    const value = node.value ? stringifyValue(node.value) : '';\n    const repeat = node.repeat ? `*${node.repeat.count}@${node.repeat.value}` : '';\n\n    return node.selfClosing && !node.value && !node.children.length\n        ? `<${name}${repeat}${attributes} />`\n        : `<${name}${repeat}${attributes}>${value}${node.children.map(elem).join('')}</${name}>`;\n\n}\n\nfunction attribute(attr: AbbreviationAttribute): string {\n    const name = attr.name || '?';\n    const value = attr.value ? `\"${stringifyValue(attr.value)}\"` : null;\n    return value != null ? `${name}=${value}` : name;\n}\n\nfunction stringifyValue(items: Value[]): string {\n    return items.map(item =>\n        typeof item === 'string'\n            ? item\n            : (item.name ? `\\${${item.index!}:${item.name}}` : `\\${${item.index!}}`)).join('');\n}\n"
  },
  {
    "path": "packages/abbreviation/test/assets/stringify.ts",
    "content": "import type { AllTokens, Repeater, RepeaterNumber, Field, OperatorType, Operator, Bracket, Quote, Literal } from '../../src/tokenizer';\nimport type { TokenElement, TokenAttribute, TokenGroup, TokenStatement } from '../../src/parser';\n\ntype TokenVisitor = <T extends AllTokens>(token: T) => string;\ninterface TokenVisitorMap {\n    [nodeType: string]: TokenVisitor;\n}\n\nconst operatorMap: { [name in OperatorType]: string } = {\n    id: '#',\n    class: '.',\n    equal: '=',\n    child: '>',\n    climb: '^',\n    sibling: '+',\n    close: '/'\n};\n\nconst tokenVisitors = {\n    Repeater(token: Repeater) {\n        return `*${token.implicit ? '' : token.count}`;\n    },\n    RepeaterNumber(token: RepeaterNumber) {\n        return '$'.repeat(token.size);\n    },\n    RepeaterPlaceholder() {\n        return '$#';\n    },\n    Field(node: Field) {\n        const index = node.index != null ? String(node.index) : '';\n        const sep = index && node.name ? ':' : '';\n        return `\\${${index}${sep}${node.name}}`;\n    },\n    Operator(node: Operator) {\n        return operatorMap[node.operator];\n    },\n    Bracket(node: Bracket) {\n        if (node.context === 'attribute') {\n            return node.open ? '[' : ']';\n        }\n\n        if (node.context === 'expression') {\n            return node.open ? '{' : '}';\n        }\n\n        if (node.context === 'group') {\n            return node.open ? '(' : ')';\n        }\n\n        return '?';\n    },\n    Quote(node: Quote) {\n        return node.single ? '\\'' : '\"';\n    },\n    Literal(node: Literal) {\n        return node.value;\n    },\n    WhiteSpace() {\n        return ' ';\n    }\n} as TokenVisitorMap;\n\nfunction statement(node: TokenElement | TokenGroup): string {\n    if (node.type === 'TokenGroup') {\n        return `(${content(node)})${node.repeat ? str(node.repeat) : ''}`;\n    }\n\n    return element(node);\n}\n\nfunction element(node: TokenElement): string {\n    const name = node.name ? tokenList(node.name) : '?';\n    const repeat = node.repeat ? str(node.repeat) : '';\n    const attributes = node.attributes ? node.attributes.map(attribute).join(' ') : '';\n    if (node.selfClose && !node.elements.length) {\n        return `<${name}${repeat}${attributes ? ' ' + attributes : ''} />`;\n    }\n\n    return `<${name}${repeat}${attributes ? ' ' + attributes : ''}>${tokenList(node.value)}${content(node)}</${name}>`;\n}\n\nfunction attribute(attr: TokenAttribute): string {\n    const name = tokenList(attr.name) || '?';\n    return attr.value ? `${name}=${tokenList(attr.value)}` : name;\n\n}\n\nfunction tokenList(tokens?: AllTokens[]): string {\n    return tokens ? tokens.map(str).join('') : '';\n}\n\nfunction str(token: AllTokens): string {\n    if (token.type in tokenVisitors) {\n        return tokenVisitors[token.type](token);\n    }\n\n    throw new Error(`Unknown token \"${token.type}\"`);\n}\n\nfunction content(node: TokenStatement): string {\n    return node.elements.map(statement).join('');\n}\n\nexport default function stringify(abbr: TokenGroup): string {\n    return content(abbr);\n}\n"
  },
  {
    "path": "packages/abbreviation/test/convert.ts",
    "content": "import { describe, it } from 'node:test';\nimport { equal } from 'node:assert';\nimport parser, { type ParserOptions } from '../src';\nimport stringify from './assets/stringify-node';\n\nfunction parse(abbr: string, options?: ParserOptions) {\n    return stringify(parser(abbr, options));\n}\n\ndescribe('Convert token abbreviations', () => {\n    it('basic', () => {\n        equal(parse('input[value=\"text$\"]*2'), '<input*2@0 value=\"text1\"></input><input*2@1 value=\"text2\"></input>');\n\n        equal(parse('ul>li.item$*3'), '<ul><li*3@0 class=\"item1\"></li><li*3@1 class=\"item2\"></li><li*3@2 class=\"item3\"></li></ul>');\n        equal(parse('ul>li.item$*', { text: ['foo$', 'bar$'] }), '<ul><li*2@0 class=\"item1\">foo$</li><li*2@1 class=\"item2\">bar$</li></ul>');\n        equal(parse('ul>li[class=$#]{item $}*', { text: ['foo$', 'bar$'] }), '<ul><li*2@0 class=\"foo$\">item 1</li><li*2@1 class=\"bar$\">item 2</li></ul>');\n        equal(parse('ul>li.item$*'), '<ul><li*1@0 class=\"item1\"></li></ul>');\n        equal(parse('ul>li.item$*', { text: ['foo.bar', 'hello.world'] }), '<ul><li*2@0 class=\"item1\">foo.bar</li><li*2@1 class=\"item2\">hello.world</li></ul>');\n\n        equal(parse('p{hi}', { text: ['hello'] }), '<p>hihello</p>');\n        equal(parse('p*{hi}', { text: ['1', '2'] }), '<p*2@0>hi1</p><p*2@1>hi2</p>');\n        equal(parse('div>p+p{hi}', { text: ['hello'] }), '<div><p></p><p>hihello</p></div>');\n\n        equal(parse('html[lang=${lang}]'), '<html lang=\"lang\"></html>');\n        equal(parse('html.one.two'), '<html class=\"one\", class=\"two\"></html>');\n        equal(parse('html.one[two=three]'), '<html class=\"one\", two=\"three\"></html>');\n        equal(parse('div{[}+a{}'), '<div>[</div><a></a>');\n    });\n\n    it('unroll', () => {\n        equal(parse('a>(b>c)+d'), '<a><b><c></c></b><d></d></a>');\n        equal(parse('(a>b)+(c>d)'), '<a><b></b></a><c><d></d></c>');\n        equal(parse('a>((b>c)(d>e))f'), '<a><b><c></c></b><d><e></e></d><f></f></a>');\n        equal(parse('a>((((b>c))))+d'), '<a><b><c></c></b><d></d></a>');\n        equal(parse('a>(((b>c))*4)+d'), '<a><b*4@0><c></c></b><b*4@1><c></c></b><b*4@2><c></c></b><b*4@3><c></c></b><d></d></a>');\n        equal(parse('(div>dl>(dt+dd)*2)'), '<div><dl><dt*2@0></dt><dd*2@0></dd><dt*2@1></dt><dd*2@1></dd></dl></div>');\n\n        equal(parse('a*2>b*3'), '<a*2@0><b*3@0></b><b*3@1></b><b*3@2></b></a><a*2@1><b*3@0></b><b*3@1></b><b*3@2></b></a>');\n        equal(parse('a>(b+c)*2'), '<a><b*2@0></b><c*2@0></c><b*2@1></b><c*2@1></c></a>');\n        equal(parse('a>(b+c)*2+(d+e)*2'), '<a><b*2@0></b><c*2@0></c><b*2@1></b><c*2@1></c><d*2@0></d><e*2@0></e><d*2@1></d><e*2@1></e></a>');\n\n        // Should move `<div>` as sibling of `{foo}`\n        equal(parse('p>{foo}>div'), '<p><?>foo</?><div></div></p>');\n        equal(parse('p>{foo ${0}}>div'), '<p><?>foo ${0}<div></div></?></p>');\n    });\n\n    it('limit unroll', () => {\n        // Limit amount of repeated elements\n        equal(parse('a*10', { maxRepeat: 5 }), '<a*10@0></a><a*10@1></a><a*10@2></a><a*10@3></a><a*10@4></a>');\n        equal(parse('a*10'), '<a*10@0></a><a*10@1></a><a*10@2></a><a*10@3></a><a*10@4></a><a*10@5></a><a*10@6></a><a*10@7></a><a*10@8></a><a*10@9></a>');\n        equal(parse('a*3>b*3', { maxRepeat: 5 }), '<a*3@0><b*3@0></b><b*3@1></b><b*3@2></b></a><a*3@1><b*3@0></b></a>');\n    });\n\n    it('parent repeater', () => {\n        equal(parse('a$*2>b$*3/'), '<a1*2@0><b1*3@0 /><b2*3@1 /><b3*3@2 /></a1><a2*2@1><b1*3@0 /><b2*3@1 /><b3*3@2 /></a2>');\n        equal(parse('a$*2>b$@^*3/'), '<a1*2@0><b1*3@0 /><b2*3@1 /><b3*3@2 /></a1><a2*2@1><b4*3@0 /><b5*3@1 /><b6*3@2 /></a2>');\n    });\n\n    it('href', () => {\n        equal(parse('a', { href: true, text: 'https://www.google.it' }), '<a href=\"https://www.google.it\">https://www.google.it</a>');\n        equal(parse('a', { href: true, text: 'www.google.it' }), '<a href=\"http://www.google.it\">www.google.it</a>');\n        equal(parse('a', { href: true, text: 'google.it' }), '<a href=\"\">google.it</a>');\n        equal(parse('a', { href: true, text: 'test here' }), '<a href=\"\">test here</a>');\n        equal(parse('a', { href: true, text: 'test@domain.com' }), '<a href=\"mailto:test@domain.com\">test@domain.com</a>');\n        equal(parse('a', { href: true, text: 'test here test@domain.com' }), '<a href=\"\">test here test@domain.com</a>');\n        equal(parse('a', { href: true, text: 'test here www.domain.com' }), '<a href=\"\">test here www.domain.com</a>');\n\n        equal(parse('a[href=]', { href: true, text: 'https://www.google.it' }), '<a href=\"https://www.google.it\">https://www.google.it</a>');\n        equal(parse('a[href=]', { href: true, text: 'www.google.it' }), '<a href=\"http://www.google.it\">www.google.it</a>');\n        equal(parse('a[href=]', { href: true, text: 'google.it' }), '<a href=\"\">google.it</a>');\n        equal(parse('a[href=]', { href: true, text: 'test here' }), '<a href=\"\">test here</a>');\n        equal(parse('a[href=]', { href: true, text: 'test@domain.com' }), '<a href=\"mailto:test@domain.com\">test@domain.com</a>');\n        equal(parse('a[href=]', { href: true, text: 'test here test@domain.com' }), '<a href=\"\">test here test@domain.com</a>');\n        equal(parse('a[href=]', { href: true, text: 'test here www.domain.com' }), '<a href=\"\">test here www.domain.com</a>');\n        equal(parse('a[class=here]', { href: true, text: 'test@domain.com' }), '<a class=\"here\", href=\"mailto:test@domain.com\">test@domain.com</a>');\n        equal(parse('a.here', { href: true, text: 'www.domain.com' }), '<a class=\"here\", href=\"http://www.domain.com\">www.domain.com</a>');\n        equal(parse('a[class=here]', { href: true, text: 'test here test@domain.com' }), '<a class=\"here\", href=\"\">test here test@domain.com</a>');\n        equal(parse('a.here', { href: true, text: 'test here www.domain.com' }), '<a class=\"here\", href=\"\">test here www.domain.com</a>');\n\n        equal(parse('a[href=\"www.google.it\"]', { href: false, text: 'test' }), '<a href=\"www.google.it\">test</a>');\n        equal(parse('a[href=\"www.example.com\"]', { href: true, text: 'www.google.it' }), '<a href=\"www.example.com\">www.google.it</a>');\n    });\n\n    it('wrap basic', () => {\n        equal(parse('p', { text: 'test' }), '<p>test</p>');\n        equal(parse('p', { text: ['test'] }), '<p>test</p>');\n        equal(parse('p', { text: ['test1', 'test2'] }), '<p>test1\\ntest2</p>');\n        equal(parse('p', { text: ['test1', '', 'test2'] }), '<p>test1\\n\\ntest2</p>');\n        equal(parse('p*', { text: ['test1', 'test2'] }), '<p*2@0>test1</p><p*2@1>test2</p>');\n        equal(parse('p*', { text: ['test1', '', 'test2'] }), '<p*2@0>test1</p><p*2@1>test2</p>');\n    })\n});\n"
  },
  {
    "path": "packages/abbreviation/test/parser.ts",
    "content": "import { describe, it } from 'node:test';\nimport { strictEqual as equal, throws } from 'node:assert';\nimport parser from '../src/parser';\nimport tokenizer from '../src/tokenizer';\nimport stringify from './assets/stringify';\nimport type { ParserOptions } from '../src';\n\nconst parse = (abbr: string, options?: ParserOptions) => parser(tokenizer(abbr), options);\nconst str = (abbr: string, options?: ParserOptions) => stringify(parse(abbr, options));\n\ndescribe('Parser', () => {\n    it('basic abbreviations', () => {\n        equal(str('p'), '<p></p>');\n        equal(str('p{text}'), '<p>text</p>');\n        equal(str('h$'), '<h$></h$>');\n        equal(str('.nav'), '<? class=nav></?>');\n        equal(str('div.width1\\\\/2'), '<div class=width1/2></div>');\n        equal(str('#sample*3'), '<?*3 id=sample></?>');\n\n        // ulmauts, https://github.com/emmetio/emmet/issues/439\n        equal(str('DatenSätze^'), '<DatenSätze></DatenSätze>')\n\n        // https://github.com/emmetio/emmet/issues/562\n        equal(str('li[repeat.for=\"todo of todoList\"]'), '<li repeat.for=\"todo of todoList\"></li>', 'Dots in attribute names');\n\n        equal(str('a>b'), '<a><b></b></a>');\n        equal(str('a+b'), '<a></a><b></b>');\n        equal(str('a+b>c+d'), '<a></a><b><c></c><d></d></b>');\n        equal(str('a>b>c+e'), '<a><b><c></c><e></e></b></a>');\n        equal(str('a>b>c^d'), '<a><b><c></c></b><d></d></a>');\n        equal(str('a>b>c^^^^d'), '<a><b><c></c></b></a><d></d>');\n        equal(str('a:b>c'), '<a:b><c></c></a:b>');\n\n        equal(str('ul.nav[title=\"foo\"]'), '<ul class=nav title=\"foo\"></ul>');\n    });\n\n    it('groups', () => {\n        equal(str('a>(b>c)+d'), '<a>(<b><c></c></b>)<d></d></a>');\n        equal(str('(a>b)+(c>d)'), '(<a><b></b></a>)(<c><d></d></c>)');\n        equal(str('a>((b>c)(d>e))f'), '<a>((<b><c></c></b>)(<d><e></e></d>))<f></f></a>');\n        equal(str('a>((((b>c))))+d'), '<a>((((<b><c></c></b>))))<d></d></a>');\n        equal(str('a>(((b>c))*4)+d'), '<a>(((<b><c></c></b>))*4)<d></d></a>');\n        equal(str('(div>dl>(dt+dd)*2)'), '(<div><dl>(<dt></dt><dd></dd>)*2</dl></div>)');\n        equal(str('a>()'), '<a>()</a>');\n    });\n\n    it('attributes', () => {\n        equal(str('[].foo'), '<? class=foo></?>');\n        equal(str('[a]'), '<? a></?>');\n        equal(str('[a b c [d]]'), '<? a b c [d]></?>');\n        equal(str('[a=b]'), '<? a=b></?>');\n        equal(str('[a=b c= d=e]'), '<? a=b c d=e></?>');\n        equal(str('[a=b.c d=тест]'), '<? a=b.c d=тест></?>');\n        equal(str('[[a]=b (c)=d]'), '<? [a]=b (c)=d></?>');\n\n        // Quoted attribute values\n        equal(str('[a=\"b\"]'), '<? a=\"b\"></?>');\n        equal(str('[a=\"b\" c=\\'d\\' e=\"\"]'), '<? a=\"b\" c=\\'d\\' e=\"\"></?>');\n        equal(str('[[a]=\"b\" (c)=\\'d\\']'), '<? [a]=\"b\" (c)=\\'d\\'></?>');\n\n        // Mixed quoted\n        equal(str('[a=\"foo\\'bar\" b=\\'foo\"bar\\' c=\"foo\\\\\\\"bar\"]'), '<? a=\"foo\\'bar\" b=\\'foo\"bar\\' c=\"foo\"bar\"></?>');\n\n        // Boolean & implied attributes\n        equal(str('[a. b.]'), '<? a. b.></?>');\n        equal(str('[!a !b.]'), '<? !a !b.></?>');\n\n        // Default values\n        equal(str('[\"a.b\"]'), '<? ?=\"a.b\"></?>');\n        equal(str('[\\'a.b\\' \"c=d\" foo=bar \"./test.html\"]'), '<? ?=\\'a.b\\' ?=\"c=d\" foo=bar ?=\"./test.html\"></?>');\n\n        // Expressions as values\n        equal(str('[foo={1 + 2} bar={fn(1, \"foo\")}]'), '<? foo={1 + 2} bar={fn(1, \"foo\")}></?>');\n\n        // Tabstops as unquoted values\n        equal(str('[name=${1} value=${2:test}]'), '<? name=${1} value=${2:test}></?>');\n    });\n\n    it('malformed attributes', () => {\n        equal(str('[a'), '<? a></?>');\n        equal(str('[a={foo]'), '<? a={foo]></?>');\n        throws(() => str('[a=\"foo]'), /Unclosed quote/);\n        throws(() => str('[a=b=c]'), /Unexpected \"Operator\" token/);\n    });\n\n    it('elements', () => {\n        equal(str('div'), '<div></div>');\n        equal(str('div.foo'), '<div class=foo></div>');\n        equal(str('div#foo'), '<div id=foo></div>');\n        equal(str('div#foo.bar'), '<div id=foo class=bar></div>');\n        equal(str('div.foo#bar'), '<div class=foo id=bar></div>');\n        equal(str('div.foo.bar.baz'), '<div class=foo class=bar class=baz></div>');\n        equal(str('.foo'), '<? class=foo></?>');\n        equal(str('#foo'), '<? id=foo></?>');\n        equal(str('.foo_bar'), '<? class=foo_bar></?>');\n        equal(str('#foo.bar'), '<? id=foo class=bar></?>');\n\n        // Attribute shorthands\n        equal(str('.'), '<? class></?>');\n        equal(str('#'), '<? id></?>');\n        equal(str('#.'), '<? id class></?>');\n        equal(str('.#.'), '<? class id class></?>');\n        equal(str('.a..'), '<? class=a class></?>');\n\n        // Elements with attributes\n        equal(str('div[foo=bar]'), '<div foo=bar></div>');\n        equal(str('div.a[b=c]'), '<div class=a b=c></div>');\n        equal(str('div.mr-\\\\[500\\\\][a=b]'), '<div class=mr-[500] a=b></div>');\n        equal(str('div[b=c].a'), '<div b=c class=a></div>');\n        equal(str('div[a=b][c=\"d\"]'), '<div a=b c=\"d\"></div>');\n        equal(str('[b=c]'), '<? b=c></?>');\n        equal(str('.a\\\\[b-c\\\\]'), '<? class=a[b-c]></?>');\n        equal(str('.\"a:[b-c]\"'), '<? class=a:[b-c]></?>');\n        equal(str('.\"peer-[.is-dirty]:peer-required:block\"'), '<? class=peer-[.is-dirty]:peer-required:block></?>');\n        equal(str('.\"mr-50\".\"peer-[:nth-of-type(3)_&]:block\"'), '<? class=mr-50 class=peer-[:nth-of-type(3)_&]:block></?>');\n        equal(str('.a[b=c]'), '<? class=a b=c></?>');\n        equal(str('[b=c].a#d'), '<? b=c class=a id=d></?>');\n        equal(str('[b=c]a'), '<? b=c></?><a></a>', 'Do not consume node name after attribute set');\n\n        // Element with text\n        equal(str('div{foo}'), '<div>foo</div>');\n        equal(str('{foo}'), '<?>foo</?>');\n\n        // Mixed\n        equal(str('div.foo{bar}'), '<div class=foo>bar</div>');\n        equal(str('.foo{bar}#baz'), '<? class=foo id=baz>bar</?>');\n        equal(str('.foo[b=c]{bar}'), '<? class=foo b=c>bar</?>');\n\n        // Repeated element\n        equal(str('div.foo*3'), '<div*3 class=foo></div>');\n        equal(str('.foo*'), '<?* class=foo></?>');\n        equal(str('.a[b=c]*10'), '<?*10 class=a b=c></?>');\n        equal(str('.a*10[b=c]'), '<?*10 class=a b=c></?>');\n        equal(str('.a*10{text}'), '<?*10 class=a>text</?>');\n\n        // Self-closing element\n        equal(str('div/'), '<div />');\n        equal(str('.foo/'), '<? class=foo />');\n        equal(str('.foo[bar]/'), '<? class=foo bar />');\n        equal(str('.foo/*3'), '<?*3 class=foo />');\n        equal(str('.foo*3/'), '<?*3 class=foo />');\n\n        throws(() => parse('/'), /Unexpected character/);\n    });\n\n    it('JSX', () => {\n        const opt = { jsx: true };\n        equal(str('foo.bar', opt), '<foo class=bar></foo>');\n        equal(str('Foo.bar', opt), '<Foo class=bar></Foo>');\n        equal(str('Foo.Bar', opt), '<Foo.Bar></Foo.Bar>');\n        equal(str('Foo.', opt), '<Foo class></Foo>');\n        equal(str('Foo.Bar.baz', opt), '<Foo.Bar class=baz></Foo.Bar>');\n        equal(str('Foo.Bar.Baz', opt), '<Foo.Bar.Baz></Foo.Bar.Baz>');\n\n        equal(str('.{theme.class}', opt), '<? class=theme.class></?>');\n        equal(str('#{id}', opt), '<? id=id></?>');\n        equal(str('Foo.{theme.class}', opt), '<Foo class=theme.class></Foo>');\n    });\n\n    it('errors', () => {\n        throws(() => parse('str?'), /Unexpected character at 4/);\n        throws(() => parse('foo,bar'), /Unexpected character at 4/);\n        equal(str('foo\\\\,bar'), '<foo,bar></foo,bar>');\n        equal(str('foo\\\\'), '<foo></foo>');\n    });\n\n    it('missing braces', () => {\n        // Do not throw errors on missing closing braces\n        equal(str('div[title=\"test\"'), '<div title=\"test\"></div>');\n        equal(str('div(foo'), '<div></div>(<foo></foo>)');\n        equal(str('div{foo'), '<div>foo</div>');\n    });\n});\n"
  },
  {
    "path": "packages/abbreviation/test/tokenizer.ts",
    "content": "import { describe, it } from 'node:test';\nimport { deepStrictEqual } from 'node:assert';\nimport tokenize from '../src/tokenizer';\n\ndescribe('Tokenizer', () => {\n    it('basic abbreviations', () => {\n        deepStrictEqual(tokenize('ul>li'), [\n            { type: 'Literal', value: 'ul', start: 0, end: 2 },\n            { type: 'Operator', operator: 'child', start: 2, end: 3 },\n            { type: 'Literal', value: 'li', start: 3, end: 5 }\n        ]);\n\n        deepStrictEqual(tokenize('ul[title=\"foo+bar\\'str\\'\" (attr)=bar]{(some > text)}'), [\n            { type: 'Literal', value: 'ul', start: 0, end: 2 },\n            { type: 'Bracket', open: true, context: 'attribute', start: 2, end: 3 },\n            { type: 'Literal', value: 'title', start: 3, end: 8 },\n            { type: 'Operator', operator: 'equal', start: 8, end: 9 },\n            { type: 'Quote', single: false, start: 9, end: 10 },\n            { type: 'Literal', value: 'foo+bar\\'str\\'', start: 10, end: 22 },\n            { type: 'Quote', single: false, start: 22, end: 23 },\n            { type: 'WhiteSpace', start: 23, end: 24, value: ' ' },\n            { type: 'Bracket', open: true, context: 'group', start: 24, end: 25 },\n            { type: 'Literal', value: 'attr', start: 25, end: 29 },\n            { type: 'Bracket', open: false, context: 'group', start: 29, end: 30 },\n            { type: 'Operator', operator: 'equal', start: 30, end: 31 },\n            { type: 'Literal', value: 'bar', start: 31, end: 34 },\n            { type: 'Bracket', open: false, context: 'attribute', start: 34, end: 35 },\n            { type: 'Bracket', open: true, context: 'expression', start: 35, end: 36 },\n            { type: 'Literal', value: '(some > text)', start: 36, end: 49 },\n            { type: 'Bracket', open: false, context: 'expression', start: 49, end: 50 }\n        ]);\n\n        deepStrictEqual(tokenize('h${some${1:field placeholder}}'), [\n            { type: 'Literal', value: 'h', start: 0, end: 1 },\n            { type: 'RepeaterNumber', size: 1, parent: 0, reverse: false, base: 1, start: 1, end: 2 },\n            { type: 'Bracket', open: true, context: 'expression', start: 2, end: 3 },\n            { type: 'Literal', value: 'some', start: 3, end: 7 },\n            { type: 'Field', index: 1, name: 'field placeholder', start: 7, end: 29 },\n            { type: 'Bracket', open: false, context: 'expression', start: 29, end: 30 }\n        ]);\n\n        deepStrictEqual(tokenize('div{[}+a{}'), [\n            { type: 'Literal', value: 'div', start: 0, end: 3 },\n            { type: 'Bracket', open: true, context: 'expression', start: 3, end: 4 },\n            { type: 'Literal', value: '[', start: 4, end: 5 },\n            { type: 'Bracket', open: false, context: 'expression', start: 5, end: 6 },\n            { type: 'Operator', operator: 'sibling', start: 6, end: 7  },\n            { type: 'Literal', value: 'a', start: 7, end: 8 },\n            { type: 'Bracket', open: true, context: 'expression', start: 8, end: 9 },\n            { type: 'Bracket', open: false, context: 'expression', start: 9, end: 10 }\n        ]);\n    });\n\n    it('repeater', () => {\n        deepStrictEqual(tokenize('#sample*3'), [\n            { type: 'Operator', operator: 'id', start: 0, end: 1 },\n            { type: 'Literal', value: 'sample', start: 1, end: 7 },\n            { type: 'Repeater', count: 3, value: 0, implicit: false, start: 7, end: 9 }\n        ]);\n\n        deepStrictEqual(tokenize('div[foo*3]'), [\n            { type: 'Literal', value: 'div', start: 0, end: 3 },\n            { type: 'Bracket', open: true, context: 'attribute', start: 3, end: 4 },\n            { type: 'Literal', value: 'foo*3', start: 4, end: 9 },\n            { type: 'Bracket', open: false, context: 'attribute', start: 9, end: 10 }\n        ]);\n\n        deepStrictEqual(tokenize('({a*2})*3'), [\n            { type: 'Bracket', open: true, context: 'group', start: 0, end: 1 },\n            { type: 'Bracket', open: true, context: 'expression', start: 1, end: 2 },\n            { type: 'Literal', value: 'a*2', start: 2, end: 5 },\n            { type: 'Bracket', open: false, context: 'expression', start: 5, end: 6 },\n            { type: 'Bracket', open: false, context: 'group', start: 6, end: 7 },\n            { type: 'Repeater', count: 3, value: 0, implicit: false, start: 7, end: 9 }\n        ]);\n    });\n});\n"
  },
  {
    "path": "packages/abbreviation/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig\",\n  \"compilerOptions\": {\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src/**/*.ts\"]\n}\n"
  },
  {
    "path": "packages/css-abbreviation/.npmignore",
    "content": "npm-debug.log*\nnode_modules\njspm_packages\n.npm\n/.*\n/*.*\n/test\n/src\n"
  },
  {
    "path": "packages/css-abbreviation/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Sergey Chikuyonok <serge.che@gmail.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/css-abbreviation/README.md",
    "content": "# Emmet stylesheet abbreviation parser\n\nParses given Emmet *stylesheet* abbreviation into AST. Parsing is performed in two steps: first it tokenizes given abbreviation (useful for syntax highlighting in editors) and then tokens are analyzed and converted into AST nodes as plain, JSON-serializable objects.\n\nUnlike in [markup abbreviations](/packages/abbreviation), elements in stylesheet abbreviations cannot be nested and contain attributes, but allow embedded values in element names.\n\n## Usage\n\nYou can install it via npm:\n\n```bash\nnpm install @emmetio/css-abbreviation\n```\n\nThen add it into your project:\n\n```js\nimport parse from '@emmetio/css-abbreviation';\n\nconst props = parse('p10+poa');\n/* [{\n    name: 'p',\n    value: [{ type: 'CSSValue', value: [...] }],\n    important: false\n}, {\n    name: 'poa',\n    value: [],\n    important: false\n}] */\n```\nThe returned result is an array of `CSSProperty` items: a node with name and values.\n\n## Abbreviation syntax\n\nEmmet stylesheet abbreviation element may start with name and followed by values, optionally chained with `-` delimiter. In most cases, actual CSS properties doesn’t have numbers in their names (or at least they are not used in abbreviation shortcuts) so a number right after alpha characters is considered as *embedded value*, as well as colors starting with `#` character: `p10`, `bg#fc0` etc. If implicit name/value boundary can’t be identified, you should use `-` as value separator: `m-a`, `p10-20` etc.\n\n### Operators\n\nSince CSS properties can’t be nested, the only available operator is `+`.\n"
  },
  {
    "path": "packages/css-abbreviation/package.json",
    "content": "{\n  \"name\": \"@emmetio/css-abbreviation\",\n  \"version\": \"2.1.8\",\n  \"description\": \"Parses Emmet CSS abbreviation into AST tree\",\n  \"main\": \"./dist/index.cjs\",\n  \"module\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"type\": \"module\",\n  \"exports\": {\n    \"import\": \"./dist/index.js\",\n    \"require\": \"./dist/index.cjs\"\n  },\n  \"scripts\": {\n    \"test\": \"tsx --test ./test/*.ts\",\n    \"build\": \"rollup -c\",\n    \"watch\": \"rollup -wc\",\n    \"clean\": \"rm -rf ./dist\",\n    \"prepublishOnly\": \"npm test &&npm run clean && npm run build\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/emmetio/emmet.git\"\n  },\n  \"keywords\": [],\n  \"author\": \"Sergey Chikuyonok <serge.che@gmail.com>\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/emmetio/emmet/issues\"\n  },\n  \"homepage\": \"https://github.com/emmetio/emmet#readme\",\n  \"dependencies\": {\n    \"@emmetio/scanner\": \"^1.0.4\"\n  },\n  \"directories\": {\n    \"test\": \"test\"\n  }\n}\n"
  },
  {
    "path": "packages/css-abbreviation/rollup.config.js",
    "content": "import typescript from '@rollup/plugin-typescript';\n\n/** @type {import('rollup').RollupOptions} */\nexport default {\n    input: './src/index.ts',\n    external: ['@emmetio/scanner'],\n    plugins: [typescript()],\n    output: [{\n        format: 'cjs',\n        file: 'dist/index.cjs',\n        sourcemap: true,\n        exports: 'named',\n    }, {\n        format: 'es',\n        file: 'dist/index.js',\n        sourcemap: true,\n    }]\n};\n"
  },
  {
    "path": "packages/css-abbreviation/src/index.ts",
    "content": "import { ScannerError } from '@emmetio/scanner';\nimport tokenize, { getToken, type AllTokens } from './tokenizer';\nimport parser, { type CSSProperty, type ParseOptions } from './parser';\n\nexport { tokenize, getToken, parser };\nexport * from './tokenizer';\nexport type { CSSProperty, CSSValue, ParseOptions, FunctionCall, Value } from './parser';\nexport type CSSAbbreviation = CSSProperty[];\n\n/**\n * Parses given abbreviation into property set\n */\nexport default function parse(abbr: string | AllTokens[], options?: ParseOptions): CSSAbbreviation {\n    try {\n        const tokens = typeof abbr === 'string' ? tokenize(abbr, options && options.value) : abbr;\n        return parser(tokens, options);\n    } catch (err) {\n        if (err instanceof ScannerError && typeof abbr === 'string') {\n            err.message += `\\n${abbr}\\n${'-'.repeat(err.pos)}^`;\n        }\n\n        throw err;\n    }\n}\n"
  },
  {
    "path": "packages/css-abbreviation/src/parser/TokenScanner.ts",
    "content": "import type { AllTokens } from '../tokenizer/index.js';\n\nexport interface TokenScanner {\n    tokens: AllTokens[];\n    start: number;\n    pos: number;\n    size: number;\n}\n\ntype TestFn = (token?: AllTokens) => boolean;\n\nexport default function tokenScanner(tokens: AllTokens[]): TokenScanner {\n    return {\n        tokens,\n        start: 0,\n        pos: 0,\n        size: tokens.length\n    };\n}\n\nexport function peek(scanner: TokenScanner): AllTokens | undefined {\n    return scanner.tokens[scanner.pos];\n}\n\nexport function next(scanner: TokenScanner): AllTokens | undefined {\n    return scanner.tokens[scanner.pos++];\n}\n\nexport function slice(scanner: TokenScanner, from = scanner.start, to = scanner.pos): AllTokens[] {\n    return scanner.tokens.slice(from, to);\n}\n\nexport function readable(scanner: TokenScanner): boolean {\n    return scanner.pos < scanner.size;\n}\n\nexport function consume(scanner: TokenScanner, test: TestFn): boolean {\n    if (test(peek(scanner))) {\n        scanner.pos++;\n        return true;\n    }\n\n    return false;\n}\n\nexport function error(scanner: TokenScanner, message: string, token = peek(scanner)) {\n    if (token && token.start != null) {\n        message += ` at ${token.start}`;\n    }\n\n    const err = new Error(message);\n    err['pos'] = token && token.start;\n\n    return err;\n}\n\nexport function consumeWhile(scanner: TokenScanner, test: TestFn): boolean {\n    const start = scanner.pos;\n    while (consume(scanner, test)) { /* */ }\n    return scanner.pos !== start;\n}\n"
  },
  {
    "path": "packages/css-abbreviation/src/parser/index.ts",
    "content": "import { OperatorType } from '../tokenizer/tokens.js';\nimport type { StringValue, NumberValue, ColorValue, Literal, AllTokens, Bracket, WhiteSpace, Operator, Field, CustomProperty } from '../tokenizer/tokens.js';\nimport tokenScanner, { type TokenScanner, readable, peek, consume, error } from './TokenScanner.js';\n\nexport type Value = StringValue | NumberValue | ColorValue | Literal | FunctionCall | Field | CustomProperty;\n\nexport interface FunctionCall {\n    type: 'FunctionCall';\n    name: string;\n    arguments: CSSValue[];\n}\n\nexport interface CSSValue {\n    type: 'CSSValue';\n    value: Value[];\n}\n\nexport interface CSSProperty {\n    name?: string;\n    value: CSSValue[];\n    important: boolean;\n    /** Snippet matched with current property */\n    snippet?: any;\n}\n\nexport interface ParseOptions {\n    /** Consumes given abbreviation tokens as value */\n    value?: boolean;\n}\n\nexport default function parser(tokens: AllTokens[], options: ParseOptions = {}): CSSProperty[] {\n    const scanner = tokenScanner(tokens);\n    const result: CSSProperty[] = [];\n    let property: CSSProperty | undefined;\n\n    while (readable(scanner)) {\n        if (property = consumeProperty(scanner, options)) {\n            result.push(property);\n        } else if (!consume(scanner, isSiblingOperator)) {\n            throw error(scanner, 'Unexpected token');\n        }\n    }\n\n    return result;\n}\n\n/**\n * Consumes single CSS property\n */\nfunction consumeProperty(scanner: TokenScanner, options: ParseOptions): CSSProperty | undefined {\n    let name: string | undefined;\n    let important = false;\n    let valueFragment: CSSValue | undefined;\n    const value: CSSValue[] = [];\n    const token = peek(scanner)!;\n    const valueMode = !!options.value;\n\n    if (!valueMode && isLiteral(token) && !isFunctionStart(scanner)) {\n        scanner.pos++;\n        name = token.value;\n        // Consume any following value delimiter after property name\n        consume(scanner, isValueDelimiter);\n    }\n\n    // Skip whitespace right after property name, if any\n    if (valueMode) {\n        consume(scanner, isWhiteSpace);\n    }\n\n    while (readable(scanner)) {\n        if (consume(scanner, isImportant)) {\n            important = true;\n        } else if (valueFragment = consumeValue(scanner, valueMode)) {\n            value.push(valueFragment);\n        } else if (!consume(scanner, isFragmentDelimiter)) {\n            break;\n        }\n    }\n\n    if (name || value.length || important) {\n        return { name, value, important };\n    }\n}\n\n/**\n * Consumes single value fragment, e.g. all value tokens before comma\n */\nfunction consumeValue(scanner: TokenScanner, inArgument: boolean): CSSValue | undefined {\n    const result: Value[] = [];\n    let token: AllTokens | undefined;\n    let args: CSSValue[] | undefined;\n\n    while (readable(scanner)) {\n        token = peek(scanner)!;\n        if (isValue(token)) {\n            scanner.pos++;\n\n            if (isLiteral(token) && (args = consumeArguments(scanner))) {\n                result.push({\n                    type: 'FunctionCall',\n                    name: token.value,\n                    arguments: args\n                } as FunctionCall);\n            } else {\n                result.push(token);\n            }\n        } else if (isValueDelimiter(token) || (inArgument && isWhiteSpace(token))) {\n            scanner.pos++;\n        } else {\n            break;\n        }\n    }\n\n    return result.length\n        ? { type: 'CSSValue', value: result }\n        : void 0;\n}\n\nfunction consumeArguments(scanner: TokenScanner): CSSValue[] | undefined {\n    const start = scanner.pos;\n    if (consume(scanner, isOpenBracket)) {\n        const args: CSSValue[] = [];\n        let value: CSSValue | undefined;\n\n        while (readable(scanner) && !consume(scanner, isCloseBracket)) {\n            if (value = consumeValue(scanner, true)) {\n                args.push(value);\n            } else if (!consume(scanner, isWhiteSpace) && !consume(scanner, isArgumentDelimiter)) {\n                throw error(scanner, 'Unexpected token');\n            }\n        }\n\n        scanner.start = start;\n        return args;\n    }\n}\n\nfunction isLiteral(token: AllTokens): token is Literal {\n    return token && token.type === 'Literal';\n}\n\nfunction isBracket(token: AllTokens, open?: boolean): token is Bracket {\n    return token && token.type === 'Bracket' && (open == null || token.open === open);\n}\n\nfunction isOpenBracket(token: AllTokens) {\n    return isBracket(token, true);\n}\n\nfunction isCloseBracket(token: AllTokens) {\n    return isBracket(token, false);\n}\n\nfunction isWhiteSpace(token: AllTokens): token is WhiteSpace {\n    return token && token.type === 'WhiteSpace';\n}\n\nfunction isOperator(token: AllTokens, operator?: OperatorType): token is Operator {\n    return token && token.type === 'Operator' && (!operator || token.operator === operator);\n}\n\nfunction isSiblingOperator(token: AllTokens) {\n    return isOperator(token, OperatorType.Sibling);\n}\n\nfunction isArgumentDelimiter(token: AllTokens) {\n    return isOperator(token, OperatorType.ArgumentDelimiter);\n}\n\nfunction isFragmentDelimiter(token: AllTokens) {\n    return isArgumentDelimiter(token);\n}\n\nfunction isImportant(token: AllTokens) {\n    return isOperator(token, OperatorType.Important);\n}\n\nfunction isValue(token: AllTokens): token is StringValue | NumberValue | ColorValue | Literal {\n    return token.type === 'StringValue'\n        || token.type === 'ColorValue'\n        || token.type === 'NumberValue'\n        || token.type === 'Literal'\n        || token.type === 'Field'\n        || token.type === 'CustomProperty';\n}\n\nfunction isValueDelimiter(token: AllTokens): boolean {\n    return isOperator(token, OperatorType.PropertyDelimiter)\n        || isOperator(token, OperatorType.ValueDelimiter);\n}\n\nfunction isFunctionStart(scanner: TokenScanner): boolean {\n    const t1 = scanner.tokens[scanner.pos];\n    const t2 = scanner.tokens[scanner.pos + 1];\n    return t1 && t2 && isLiteral(t1) && t2.type === 'Bracket';\n}\n"
  },
  {
    "path": "packages/css-abbreviation/src/tokenizer/index.ts",
    "content": "import { default as Scanner, isAlphaWord, isAlpha, isNumber, isAlphaNumericWord, isSpace, isQuote } from '@emmetio/scanner';\nimport { OperatorType } from './tokens';\nimport type { AllTokens, Literal, NumberValue, ColorValue, WhiteSpace, Operator, Bracket, StringValue, Field, CustomProperty } from './tokens';\nimport { Chars } from './utils';\n\nexport * from './tokens';\n\nexport default function tokenize(abbr: string, isValue?: boolean): AllTokens[] {\n    let brackets = 0;\n    let token: AllTokens | undefined;\n    const scanner = new Scanner(abbr);\n    const tokens: AllTokens[] = [];\n\n    while (!scanner.eof()) {\n        token = getToken(scanner, brackets === 0 && !isValue);\n\n        if (!token) {\n            throw scanner.error('Unexpected character');\n        }\n\n        if (token.type === 'Bracket') {\n            if (!brackets && token.open) {\n                mergeTokens(scanner, tokens);\n            }\n\n            brackets += token.open ? 1 : -1;\n            if (brackets < 0) {\n                throw scanner.error('Unexpected bracket', token.start);\n            }\n        }\n\n        tokens.push(token);\n\n        // Forcibly consume next operator after unit-less numeric value or color:\n        // next dash `-` must be used as value delimiter\n        if (shouldConsumeDashAfter(token) && (token = operator(scanner))) {\n            tokens.push(token);\n        }\n    }\n\n    return tokens;\n}\n\n/**\n * Returns next token from given scanner, if possible\n */\nexport function getToken(scanner: Scanner, short?: boolean) {\n    return field(scanner)\n        || customProperty(scanner)\n        || numberValue(scanner)\n        || colorValue(scanner)\n        || stringValue(scanner)\n        || bracket(scanner)\n        || operator(scanner)\n        || whiteSpace(scanner)\n        || literal(scanner, short);\n}\n\nfunction field(scanner: Scanner): Field | undefined {\n    const start = scanner.pos;\n    if (scanner.eat(Chars.Dollar) && scanner.eat(Chars.CurlyBracketOpen)) {\n        scanner.start = scanner.pos;\n\n        let index: number | undefined;\n        let name: string = '';\n\n        if (scanner.eatWhile(isNumber)) {\n            // It’s a field\n            index = Number(scanner.current());\n            name = scanner.eat(Chars.Colon) ? consumePlaceholder(scanner) : '';\n        } else if (isAlpha(scanner.peek())) {\n            // It’s a variable\n            name = consumePlaceholder(scanner);\n        }\n\n        if (scanner.eat(Chars.CurlyBracketClose)) {\n            return {\n                type: 'Field',\n                index, name,\n                start,\n                end: scanner.pos\n            };\n        }\n\n        throw scanner.error('Expecting }');\n    }\n\n    // If we reached here then there’s no valid field here, revert\n    // back to starting position\n    scanner.pos = start;\n}\n\n/**\n * Consumes a placeholder: value right after `:` in field. Could be empty\n */\nfunction consumePlaceholder(stream: Scanner): string {\n    const stack: number[] = [];\n    stream.start = stream.pos;\n\n    while (!stream.eof()) {\n        if (stream.eat(Chars.CurlyBracketOpen)) {\n            stack.push(stream.pos);\n        } else if (stream.eat(Chars.CurlyBracketClose)) {\n            if (!stack.length) {\n                stream.pos--;\n                break;\n            }\n            stack.pop();\n        } else {\n            stream.pos++;\n        }\n    }\n\n    if (stack.length) {\n        stream.pos = stack.pop()!;\n        throw stream.error(`Expecting }`);\n    }\n\n    return stream.current();\n}\n\n/**\n * Consumes literal from given scanner\n * @param short Use short notation for consuming value.\n * The difference between “short” and “full” notation is that first one uses\n * alpha characters only and used for extracting keywords from abbreviation,\n * while “full” notation also supports numbers and dashes\n */\nfunction literal(scanner: Scanner, short?: boolean): Literal | undefined {\n    const start = scanner.pos;\n\n    if (scanner.eat(isIdentPrefix)) {\n        // SCSS or LESS variable\n        // NB a bit dirty hack: if abbreviation starts with identifier prefix,\n        // consume alpha characters only to allow embedded variables\n        scanner.eatWhile(start ? isKeyword : isLiteral);\n    } else if (scanner.eat(isAlphaWord)) {\n        scanner.eatWhile(short ? isLiteral : isKeyword);\n    } else {\n        // Allow dots only at the beginning of literal\n        scanner.eat(Chars.Dot);\n        scanner.eatWhile(isLiteral);\n    }\n\n    if (start !== scanner.pos) {\n        scanner.start = start;\n        return createLiteral(scanner, scanner.start = start);\n    }\n}\n\nfunction createLiteral(scanner: Scanner, start = scanner.start, end = scanner.pos): Literal {\n    return {\n        type: 'Literal',\n        value: scanner.substring(start, end),\n        start,\n        end\n    };\n}\n\n/**\n * Consumes numeric CSS value (number with optional unit) from current stream,\n * if possible\n */\nfunction numberValue(scanner: Scanner): NumberValue | undefined {\n    const start = scanner.pos;\n    if (consumeNumber(scanner)) {\n        scanner.start = start;\n        const rawValue = scanner.current();\n\n        // eat unit, which can be a % or alpha word\n        scanner.start = scanner.pos;\n        scanner.eat(Chars.Percent) || scanner.eatWhile(isAlphaWord);\n        return {\n            type: 'NumberValue',\n            value: Number(rawValue),\n            rawValue,\n            unit: scanner.current(),\n            start,\n            end: scanner.pos\n        };\n    }\n}\n\n/**\n * Consumes quoted string value from given scanner\n */\nfunction stringValue(scanner: Scanner): StringValue | undefined {\n    const ch = scanner.peek();\n    const start = scanner.pos;\n    let finished = false;\n\n    if (isQuote(ch)) {\n        scanner.pos++;\n        while (!scanner.eof()) {\n            // Do not throw error on malformed string\n            if (scanner.eat(ch)) {\n                finished = true;\n                break;\n            } else {\n                scanner.pos++;\n            }\n        }\n\n        scanner.start = start;\n        return {\n            type: 'StringValue',\n            value: scanner.substring(start + 1, scanner.pos - (finished ? 1 : 0)),\n            quote: ch === Chars.SingleQuote ? 'single' : 'double',\n            start,\n            end: scanner.pos\n        };\n    }\n}\n\n/**\n * Consumes a color token from given string\n */\nfunction colorValue(scanner: Scanner): ColorValue | Literal | undefined {\n    // supported color variations:\n    // #abc   → #aabbccc\n    // #0     → #000000\n    // #fff.5 → rgba(255, 255, 255, 0.5)\n    // #t     → transparent\n    const start = scanner.pos;\n    if (scanner.eat(Chars.Hash)) {\n        const valueStart = scanner.pos;\n        let color = '';\n        let alpha = '';\n        if (scanner.eatWhile(isHex)) {\n            color = scanner.substring(valueStart, scanner.pos);\n            alpha = colorAlpha(scanner);\n        } else if (scanner.eat(Chars.Transparent)) {\n            color = '0';\n            alpha = colorAlpha(scanner) || '0';\n        } else {\n            alpha = colorAlpha(scanner);\n        }\n\n        if (color || alpha || scanner.eof()) {\n            const { r, g, b, a } = parseColor(color, alpha);\n            return {\n                type: 'ColorValue',\n                r, g, b, a,\n                raw: scanner.substring(start + 1, scanner.pos),\n                start,\n                end: scanner.pos\n            };\n        } else {\n            // Consumed # but no actual value: invalid color value, treat it as literal\n            return createLiteral(scanner, start);\n        }\n    }\n    scanner.pos = start;\n}\n\n/**\n * Consumes alpha value of color: `.1`\n */\nfunction colorAlpha(scanner: Scanner): string {\n    const start = scanner.pos;\n    if (scanner.eat(Chars.Dot)) {\n        scanner.start = start;\n        if (scanner.eatWhile(isNumber)) {\n            return scanner.current();\n        }\n        return '1';\n    }\n\n    return '';\n}\n\n/**\n * Consumes white space characters as string literal from given scanner\n */\nfunction whiteSpace(scanner: Scanner): WhiteSpace | undefined {\n    const start = scanner.pos;\n    if (scanner.eatWhile(isSpace)) {\n        return {\n            type: 'WhiteSpace',\n            start,\n            end: scanner.pos\n        };\n    }\n}\n\n/**\n * Consumes custom CSS property: --foo-bar\n */\nfunction customProperty(scanner: Scanner): CustomProperty | undefined {\n    const start = scanner.pos;\n    if (scanner.eat(Chars.Dash) && scanner.eat(Chars.Dash)) {\n        scanner.start = start;\n        scanner.eatWhile(isKeyword);\n\n        return {\n            type: 'CustomProperty',\n            value: scanner.current(),\n            start,\n            end: scanner.pos\n        };\n    }\n\n    scanner.pos = start;\n}\n\n/**\n * Consumes bracket from given scanner\n */\nfunction bracket(scanner: Scanner): Bracket | undefined {\n    const ch = scanner.peek();\n    if (isBracket(ch)) {\n        return {\n            type: 'Bracket',\n            open: ch === Chars.RoundBracketOpen,\n            start: scanner.pos++,\n            end: scanner.pos\n        };\n    }\n}\n\n/**\n * Consumes operator from given scanner\n */\nfunction operator(scanner: Scanner): Operator | undefined {\n    const op = operatorType(scanner.peek());\n    if (op) {\n        return {\n            type: 'Operator',\n            operator: op,\n            start: scanner.pos++,\n            end: scanner.pos\n        };\n    }\n}\n\n/**\n * Eats number value from given stream\n * @return Returns `true` if number was consumed\n */\nfunction consumeNumber(stream: Scanner): boolean {\n    const start = stream.pos;\n    stream.eat(Chars.Dash);\n    const afterNegative = stream.pos;\n\n    const hasDecimal = stream.eatWhile(isNumber);\n\n    const prevPos = stream.pos;\n    if (stream.eat(Chars.Dot)) {\n        // It’s perfectly valid to have numbers like `1.`, which enforces\n        // value to float unit type\n        const hasFloat = stream.eatWhile(isNumber);\n        if (!hasDecimal && !hasFloat) {\n            // Lone dot\n            stream.pos = prevPos;\n        }\n    }\n\n    // Edge case: consumed dash only: not a number, bail-out\n    if (stream.pos === afterNegative) {\n        stream.pos = start;\n    }\n\n    return stream.pos !== start;\n}\n\nfunction isIdentPrefix(code: number): boolean {\n    return code === Chars.At || code === Chars.Dollar;\n}\n\n/**\n * If given character is an operator, returns it’s type\n */\nfunction operatorType(ch: number): OperatorType | undefined {\n    return (ch === Chars.Sibling && OperatorType.Sibling)\n        || (ch === Chars.Excl && OperatorType.Important)\n        || (ch === Chars.Comma && OperatorType.ArgumentDelimiter)\n        || (ch === Chars.Colon && OperatorType.PropertyDelimiter)\n        || (ch === Chars.Dash && OperatorType.ValueDelimiter)\n        || void 0;\n}\n\n/**\n * Check if given code is a hex value (/0-9a-f/)\n */\nfunction isHex(code: number): boolean {\n    return isNumber(code) || isAlpha(code, 65, 70); // A-F\n}\n\nfunction isKeyword(code: number): boolean {\n    return isAlphaNumericWord(code) || code === Chars.Dash;\n}\n\nfunction isBracket(code: number) {\n    return code === Chars.RoundBracketOpen || code === Chars.RoundBracketClose;\n}\n\nfunction isLiteral(code: number) {\n    return isAlphaWord(code) || code === Chars.Percent || code === Chars.Slash;\n}\n\n/**\n * Parses given color value from abbreviation into RGBA format\n */\nfunction parseColor(value: string, alpha?: string): { r: number, g: number, b: number, a: number } {\n    let r = '0';\n    let g = '0';\n    let b = '0';\n    let a = Number(alpha != null && alpha !== '' ? alpha : 1);\n\n    if (value === 't') {\n        a = 0;\n    } else {\n        switch (value.length) {\n            case 0:\n                break;\n\n            case 1:\n                r = g = b = value + value;\n                break;\n\n            case 2:\n                r = g = b = value;\n                break;\n\n            case 3:\n                r = value[0] + value[0];\n                g = value[1] + value[1];\n                b = value[2] + value[2];\n                break;\n\n            default:\n                value += value;\n                r = value.slice(0, 2);\n                g = value.slice(2, 4);\n                b = value.slice(4, 6);\n        }\n    }\n\n    return {\n        r: parseInt(r, 16),\n        g: parseInt(g, 16),\n        b: parseInt(b, 16),\n        a\n    };\n}\n\n/**\n * Check if scanner reader must consume dash after given token.\n * Used in cases where user must explicitly separate numeric values\n */\nfunction shouldConsumeDashAfter(token: AllTokens): boolean {\n    return token.type === 'ColorValue' || (token.type === 'NumberValue' && !token.unit);\n}\n\n/**\n * Merges last adjacent tokens into a single literal.\n * This function is used to overcome edge case when function name was parsed\n * as a list of separate tokens. For example, a `scale3d()` value will be\n * parsed as literal and number tokens (`scale` and `3d`) which is a perfectly\n * valid abbreviation but undesired result. This function will detect last adjacent\n * literal and number values and combine them into single literal\n */\nfunction mergeTokens(scanner: Scanner, tokens: AllTokens[]) {\n    let start = 0;\n    let end = 0;\n\n    while (tokens.length) {\n        const token = last(tokens)!;\n        if (token.type === 'Literal' || token.type === 'NumberValue') {\n            start = token.start!;\n            if (!end) {\n                end = token.end!;\n            }\n            tokens.pop();\n        } else {\n            break;\n        }\n    }\n\n    if (start !== end) {\n        tokens.push(createLiteral(scanner, start, end));\n    }\n}\n\nfunction last<T>(arr: T[]): T | undefined {\n    return arr[arr.length - 1];\n}\n"
  },
  {
    "path": "packages/css-abbreviation/src/tokenizer/tokens.ts",
    "content": "export type AllTokens = Bracket | Literal | Operator | WhiteSpace | ColorValue\n    | NumberValue | StringValue | CustomProperty | Field;\n\nexport const enum OperatorType {\n    Sibling = '+',\n    Important = '!',\n    ArgumentDelimiter = ',',\n    ValueDelimiter = '-',\n    PropertyDelimiter = ':'\n}\n\nexport interface Token {\n    type: string;\n\n    /** Location of token start in source */\n    start?: number;\n\n    /** Location of token end in source */\n    end?: number;\n}\n\nexport interface Operator extends Token {\n    type: 'Operator';\n    operator: OperatorType;\n}\n\nexport interface Bracket extends Token {\n    type: 'Bracket';\n    open: boolean;\n}\n\nexport interface Literal extends Token {\n    type: 'Literal';\n    value: string;\n}\n\nexport interface CustomProperty extends Token {\n    type: 'CustomProperty';\n    value: string;\n}\n\nexport interface NumberValue extends Token {\n    type: 'NumberValue';\n    value: number;\n    unit: string;\n    rawValue: string;\n}\n\nexport interface ColorValue extends Token {\n    type: 'ColorValue';\n    r: number;\n    g: number;\n    b: number;\n    a: number;\n    raw: string;\n}\n\nexport interface StringValue extends Token {\n    type: 'StringValue';\n    value: string;\n    quote: 'single' | 'double';\n}\n\nexport interface WhiteSpace extends Token {\n    type: 'WhiteSpace';\n}\n\nexport interface Field extends Token {\n    type: 'Field';\n    index?: number;\n    name: string;\n}\n"
  },
  {
    "path": "packages/css-abbreviation/src/tokenizer/utils.ts",
    "content": "export const enum Chars {\n    /** `#` character */\n    Hash = 35,\n\n    /** `$` character */\n    Dollar = 36,\n\n    /** `-` character */\n    Dash = 45,\n\n    /** `.` character */\n    Dot = 46,\n\n    /** `:` character */\n    Colon = 58,\n\n    /** `,` character */\n    Comma = 44,\n\n    /** `!` character */\n    Excl = 33,\n\n    /** `@` character */\n    At = 64,\n\n    /** `%` character */\n    Percent = 37,\n\n    /** `_` character */\n    Underscore = 95,\n\n    /** `(` character */\n    RoundBracketOpen = 40,\n\n    /** `)` character */\n    RoundBracketClose = 41,\n\n    /** `{` character */\n    CurlyBracketOpen = 123,\n\n    /** `}` character */\n    CurlyBracketClose = 125,\n\n    /** `+` character */\n    Sibling = 43,\n\n    /** `'` character */\n    SingleQuote = 39,\n\n    /** `\"` character */\n    DoubleQuote = 34,\n\n    /** `t` character */\n    Transparent = 116,\n\n    /** `/` character */\n    Slash = 47,\n}\n"
  },
  {
    "path": "packages/css-abbreviation/test/assets/stringify.ts",
    "content": "import type { CSSProperty, CSSValue, Value } from '../../src/parser';\n\nexport default function stringify(prop: CSSProperty): string {\n    return `${prop.name || '?'}: ${prop.value.map(stringifyValue).join(', ')}${prop.important ? ' !important' : ''};`;\n}\n\nfunction stringifyValue(value: CSSValue): string {\n    return value.value.map(stringifyToken).join(' ');\n}\n\nfunction stringifyToken(token: Value): string {\n    if (token.type === 'ColorValue') {\n        const { r, g, b, a } = token;\n        if (!r && !g && !b && !a) {\n            return 'transparent';\n        }\n\n        if (a === 1) {\n            return `#${toHex(r)}${toHex(g)}${toHex(b)}`;\n        }\n\n        return `rgba(${r}, ${g}, ${b}, ${a})`;\n    } else if (token.type === 'NumberValue') {\n        return `${token.value}${token.unit}`;\n    } else if (token.type === 'StringValue') {\n        return `\"${token.value}\"`;\n    } else if (token.type === 'Literal') {\n        return token.value;\n    } else if (token.type === 'FunctionCall') {\n        return `${token.name}(${token.arguments.map(stringifyValue).join(', ')})`;\n    }\n\n    throw new Error('Unexpected token');\n}\n\nfunction toHex(num: number): string {\n    return pad(num.toString(16), 2);\n}\n\nfunction pad(value: string, len: number): string {\n    while (value.length < len) {\n        value = '0' + value;\n    }\n    return value;\n}\n"
  },
  {
    "path": "packages/css-abbreviation/test/parser.ts",
    "content": "import { describe, it } from 'node:test';\nimport { strictEqual as equal, throws } from 'node:assert';\nimport parser, { ParseOptions, FunctionCall } from '../src/parser';\nimport tokenizer from '../src/tokenizer';\nimport stringify from './assets/stringify';\n\nconst parse = (abbr: string, opt?: ParseOptions) => parser(tokenizer(abbr), opt);\nconst expand = (abbr: string) => parse(abbr).map(stringify).join('');\n\ndescribe('CSS Abbreviation parser', () => {\n    it('basic', () => {\n        equal(expand('p10!'), 'p: 10 !important;');\n        equal(expand('p-10-20'), 'p: -10 20;');\n        equal(expand('p-10%-20--30'), 'p: -10% -20 -30;');\n        equal(expand('p.5'), 'p: 0.5;');\n        equal(expand('p-.5'), 'p: -0.5;');\n        equal(expand('p.1.2.3'), 'p: 0.1 0.2 0.3;');\n        equal(expand('p.1-.2.3'), 'p: 0.1 0.2 0.3;');\n        equal(expand('10'), '?: 10;');\n        equal(expand('.1'), '?: 0.1;');\n        equal(expand('lh1.'), 'lh: 1;');\n    });\n\n    it('color', () => {\n        equal(expand('c#'), 'c: #000000;');\n        equal(expand('c#1'), 'c: #111111;');\n        equal(expand('c#f'), 'c: #ffffff;');\n        equal(expand('c#a#b#c'), 'c: #aaaaaa #bbbbbb #cccccc;');\n        equal(expand('c#af'), 'c: #afafaf;');\n        equal(expand('c#fc0'), 'c: #ffcc00;');\n        equal(expand('c#11.5'), 'c: rgba(17, 17, 17, 0.5);');\n        equal(expand('c#.99'), 'c: rgba(0, 0, 0, 0.99);');\n        equal(expand('c#t'), 'c: transparent;');\n    });\n\n    it('keywords', () => {\n        equal(expand('m:a'), 'm: a;');\n        equal(expand('m-a'), 'm: a;');\n        equal(expand('m-abc'), 'm: abc;');\n        equal(expand('m-a0'), 'm: a 0;');\n        equal(expand('m-a0-a'), 'm: a 0 a;');\n    });\n\n    it('functions', () => {\n        equal(expand('bg-lg(top,   \"red, black\",rgb(0, 0, 0) 10%)'), 'bg: lg(top, \"red, black\", rgb(0, 0, 0) 10%);');\n        equal(expand('lg(top,   \"red, black\",rgb(0, 0, 0) 10%)'), '?: lg(top, \"red, black\", rgb(0, 0, 0) 10%);');\n    });\n\n    it('mixed', () => {\n        equal(expand('bd1-s#fc0'), 'bd: 1 s #ffcc00;');\n        equal(expand('bd#fc0-1'), 'bd: #ffcc00 1;');\n        equal(expand('p0+m0'), 'p: 0;m: 0;');\n        equal(expand('p0!+m0!'), 'p: 0 !important;m: 0 !important;');\n    });\n\n    it('embedded variables', () => {\n        equal(expand('foo$bar'), 'foo: $bar;');\n        equal(expand('foo$bar-2'), 'foo: $bar-2;');\n        equal(expand('foo$bar@bam'), 'foo: $bar @bam;');\n    });\n\n    it('parse value', () => {\n        const opt: ParseOptions = { value: true };\n        let prop = parse('${1:foo} ${2:bar}, baz', opt)[0];\n        equal(prop.name, undefined);\n        equal(prop.value.length, 2);\n        equal(prop.value[0].value.length, 2);\n        equal(prop.value[0].value[0].type, 'Field');\n\n        prop = parse('scale3d(${1:x}, ${2:y}, ${3:z})', opt)[0];\n        const fn = prop.value[0].value[0] as FunctionCall;\n        equal(prop.value.length, 1);\n        equal(prop.value[0].value.length, 1);\n        equal(fn.type, 'FunctionCall');\n        equal(fn.name, 'scale3d');\n\n        prop = parse('repeat(2,auto) / repeat(auto-fit, minmax(250px, 1fr))', opt)[0];\n        equal(prop.value.length, 1);\n    });\n\n    it('errors', () => {\n        throws(() => expand('p10 '), /Unexpected token/);\n    });\n});\n"
  },
  {
    "path": "packages/css-abbreviation/test/tokenizer.ts",
    "content": "import { describe, it } from 'node:test';\nimport { deepStrictEqual as deepEqual } from 'node:assert';\nimport tokenize from '../src/tokenizer';\n\ndescribe('Tokenizer', () => {\n    it('numeric values', () => {\n        deepEqual(tokenize('p10'), [\n            { type: 'Literal', value: 'p', start: 0, end: 1 },\n            { type: 'NumberValue', value: 10, rawValue: '10', unit: '', start: 1, end: 3 }\n        ]);\n\n        deepEqual(tokenize('p-10'), [\n            { type: 'Literal', value: 'p', start: 0, end: 1 },\n            { type: 'NumberValue', value: -10, rawValue: '-10', unit: '', start: 1, end: 4 }\n        ]);\n\n        deepEqual(tokenize('p-10-'), [\n            { type: 'Literal', value: 'p', start: 0, end: 1 },\n            { type: 'NumberValue', value: -10, rawValue: '-10', unit: '', start: 1, end: 4 },\n            { type: 'Operator', operator: '-', start: 4, end: 5 }\n        ]);\n\n        deepEqual(tokenize('p-10-20'), [\n            { type: 'Literal', value: 'p', start: 0, end: 1 },\n            { type: 'NumberValue', value: -10, rawValue: '-10', unit: '', start: 1, end: 4 },\n            { type: 'Operator', operator: '-', start: 4, end: 5 },\n            { type: 'NumberValue', value: 20, rawValue: '20', unit: '', start: 5, end: 7 }\n        ]);\n\n        deepEqual(tokenize('p-10--20'), [\n            { type: 'Literal', value: 'p', start: 0, end: 1 },\n            { type: 'NumberValue', value: -10, rawValue: '-10', unit: '', start: 1, end: 4 },\n            { type: 'Operator', operator: '-', start: 4, end: 5 },\n            { type: 'NumberValue', value: -20, rawValue: '-20', unit: '', start: 5, end: 8 }\n        ]);\n\n        deepEqual(tokenize('p-10-20--30'), [\n            { type: 'Literal', value: 'p', start: 0, end: 1 },\n            { type: 'NumberValue', value: -10, rawValue: '-10', unit: '', start: 1, end: 4 },\n            { type: 'Operator', operator: '-', start: 4, end: 5 },\n            { type: 'NumberValue', value: 20, rawValue: '20', unit: '', start: 5, end: 7 },\n            { type: 'Operator', operator: '-', start: 7, end: 8 },\n            { type: 'NumberValue', value: -30, rawValue: '-30', unit: '', start: 8, end: 11 }\n        ]);\n\n        deepEqual(tokenize('p-10p-20--30'), [\n            { type: 'Literal', value: 'p', start: 0, end: 1 },\n            { type: 'NumberValue', value: -10, rawValue: '-10', unit: 'p', start: 1, end: 5 },\n            { type: 'NumberValue', value: -20, rawValue: '-20', unit: '', start: 5, end: 8 },\n            { type: 'Operator', operator: '-', start: 8, end: 9 },\n            { type: 'NumberValue', value: -30, rawValue: '-30', unit: '', start: 9, end: 12 }\n        ]);\n\n        deepEqual(tokenize('p-10%-20--30'), [\n            { type: 'Literal', value: 'p', start: 0, end: 1 },\n            { type: 'NumberValue', value: -10, rawValue: '-10', unit: '%', start: 1, end: 5 },\n            { type: 'NumberValue', value: -20, rawValue: '-20', unit: '', start: 5, end: 8 },\n            { type: 'Operator', operator: '-', start: 8, end: 9 },\n            { type: 'NumberValue', value: -30, rawValue: '-30', unit: '', start: 9, end: 12 }\n        ]);\n    });\n\n    it('float values', () => {\n        deepEqual(tokenize('p.5'), [\n            { type: 'Literal', value: 'p', start: 0, end: 1 },\n            { type: 'NumberValue', value: 0.5, rawValue: '.5', unit: '', start: 1, end: 3 }\n        ]);\n\n        deepEqual(tokenize('p-.5'), [\n            { type: 'Literal', value: 'p', start: 0, end: 1 },\n            { type: 'NumberValue', value: -0.5, rawValue: '-.5', unit: '', start: 1, end: 4 }\n        ]);\n\n        deepEqual(tokenize('p.1.2.3'), [\n            { type: 'Literal', value: 'p', start: 0, end: 1 },\n            { type: 'NumberValue', value: 0.1, rawValue: '.1', unit: '', start: 1, end: 3 },\n            { type: 'NumberValue', value: 0.2, rawValue: '.2', unit: '', start: 3, end: 5 },\n            { type: 'NumberValue', value: 0.3, rawValue: '.3', unit: '', start: 5, end: 7 }\n        ]);\n\n        deepEqual(tokenize('p.1-.2.3'), [\n            { type: 'Literal', value: 'p', start: 0, end: 1 },\n            { type: 'NumberValue', value: 0.1, rawValue: '.1', unit: '', start: 1, end: 3 },\n            { type: 'Operator', operator: '-', start: 3, end: 4 },\n            { type: 'NumberValue', value: 0.2, rawValue: '.2', unit: '', start: 4, end: 6 },\n            { type: 'NumberValue', value: 0.3, rawValue: '.3', unit: '', start: 6, end: 8 }\n        ]);\n\n        deepEqual(tokenize('p.1--.2.3'), [\n            { type: 'Literal', value: 'p', start: 0, end: 1 },\n            { type: 'NumberValue', value: 0.1, rawValue: '.1', unit: '', start: 1, end: 3 },\n            { type: 'Operator', operator: '-', start: 3, end: 4 },\n            { type: 'NumberValue', value: -0.2, rawValue: '-.2', unit: '', start: 4, end: 7 },\n            { type: 'NumberValue', value: 0.3, rawValue: '.3', unit: '', start: 7, end: 9 }\n        ]);\n\n        deepEqual(tokenize('10'), [\n            { type: 'NumberValue', value: 10, rawValue: '10', unit: '', start: 0, end: 2 },\n        ]);\n\n        deepEqual(tokenize('.1'), [\n            { type: 'NumberValue', value: 0.1, rawValue: '.1', unit: '', start: 0, end: 2 },\n        ]);\n\n        // NB: now dot should be a part of literal\n        // throws(() => tokenize('.foo'), /Unexpected character at 1/);\n    });\n\n    it('color values', () => {\n        deepEqual(tokenize('c#'), [\n            { type: 'Literal', value: 'c', start: 0, end: 1 },\n            { type: 'ColorValue', r: 0, g: 0, b: 0, a: 1, raw: '', start: 1, end: 2 }\n        ]);\n\n        deepEqual(tokenize('c#1'), [\n            { type: 'Literal', value: 'c', start: 0, end: 1 },\n            { type: 'ColorValue', r: 17, g: 17, b: 17, a: 1, raw: '1', start: 1, end: 3 }\n        ]);\n\n        deepEqual(tokenize('c#.'), [\n            { type: 'Literal', value: 'c', start: 0, end: 1 },\n            { type: 'ColorValue', r: 0, g: 0, b: 0, a: 1, raw: '.', start: 1, end: 3 }\n        ]);\n\n        deepEqual(tokenize('c#f'), [\n            { type: 'Literal', value: 'c', start: 0, end: 1 },\n            { type: 'ColorValue', r: 255, g: 255, b: 255, a: 1, raw: 'f', start: 1, end: 3 }\n        ]);\n\n        deepEqual(tokenize('c#a#b#c'), [\n            { type: 'Literal', value: 'c', start: 0, end: 1 },\n            { type: 'ColorValue', r: 170, g: 170, b: 170, a: 1, raw: 'a', start: 1, end: 3 },\n            { type: 'ColorValue', r: 187, g: 187, b: 187, a: 1, raw: 'b', start: 3, end: 5 },\n            { type: 'ColorValue', r: 204, g: 204, b: 204, a: 1, raw: 'c', start: 5, end: 7 }\n        ]);\n\n        deepEqual(tokenize('c#af'), [\n            { type: 'Literal', value: 'c', start: 0, end: 1 },\n            { type: 'ColorValue', r: 175, g: 175, b: 175, a: 1, raw: 'af', start: 1, end: 4 }\n        ]);\n\n        deepEqual(tokenize('c#fc0'), [\n            { type: 'Literal', value: 'c', start: 0, end: 1 },\n            { type: 'ColorValue', r: 255, g: 204, b: 0, a: 1, raw: 'fc0', start: 1, end: 5 }\n        ]);\n\n        deepEqual(tokenize('c#11.5'), [\n            { type: 'Literal', value: 'c', start: 0, end: 1 },\n            { type: 'ColorValue', r: 17, g: 17, b: 17, a: 0.5, raw: '11.5', start: 1, end: 6 }\n        ]);\n\n        deepEqual(tokenize('c#.99'), [\n            { type: 'Literal', value: 'c', start: 0, end: 1 },\n            { type: 'ColorValue', r: 0, g: 0, b: 0, a: 0.99, raw: '.99', start: 1, end: 5 }\n        ]);\n\n        deepEqual(tokenize('c#t'), [\n            { type: 'Literal', value: 'c', start: 0, end: 1 },\n            { type: 'ColorValue', r: 0, g: 0, b: 0, a: 0, raw: 't', start: 1, end: 3 }\n        ]);\n\n        deepEqual(tokenize('c#${fff}'), [\n            { type: 'Literal', value: 'c', start: 0, end: 1 },\n            { type: 'Literal', value: '#', start: 1, end: 2 },\n            { type: 'Field', index: undefined, name: 'fff', start: 2, end: 8 }\n        ]);\n    });\n\n    it('keywords', () => {\n        deepEqual(tokenize('m:a'), [\n            { type: 'Literal', value: 'm', start: 0, end: 1 },\n            { type: 'Operator', operator: ':', start: 1, end: 2 },\n            { type: 'Literal', value: 'a', start: 2, end: 3 }\n        ]);\n\n        deepEqual(tokenize('m-a'), [\n            { type: 'Literal', value: 'm', start: 0, end: 1 },\n            { type: 'Operator', operator: '-', start: 1, end: 2 },\n            { type: 'Literal', value: 'a', start: 2, end: 3 }\n        ]);\n\n        deepEqual(tokenize('m-abc'), [\n            { type: 'Literal', value: 'm', start: 0, end: 1 },\n            { type: 'Operator', operator: '-', start: 1, end: 2 },\n            { type: 'Literal', value: 'abc', start: 2, end: 5 }\n        ]);\n\n        deepEqual(tokenize('m-a0'), [\n            { type: 'Literal', value: 'm', start: 0, end: 1 },\n            { type: 'Operator', operator: '-', start: 1, end: 2 },\n            { type: 'Literal', value: 'a', start: 2, end: 3 },\n            { type: 'NumberValue', value: 0, rawValue: '0', unit: '', start: 3, end: 4 }\n        ]);\n\n        deepEqual(tokenize('m-a0-a'), [\n            { type: 'Literal', value: 'm', start: 0, end: 1 },\n            { type: 'Operator', operator: '-', start: 1, end: 2 },\n            { type: 'Literal', value: 'a', start: 2, end: 3 },\n            { type: 'NumberValue', value: 0, rawValue: '0', unit: '', start: 3, end: 4 },\n            { type: 'Operator', operator: '-', start: 4, end: 5 },\n            { type: 'Literal', value: 'a', start: 5, end: 6 }\n        ]);\n    });\n\n    it('arguments', () => {\n        deepEqual(tokenize('lg(top, \"red, black\", rgb(0, 0, 0) 10%)'), [\n            { type: 'Literal', value: 'lg', start: 0, end: 2 },\n            { type: 'Bracket', open: true, start: 2, end: 3 },\n            { type: 'Literal', value: 'top', start: 3, end: 6 },\n            { type: 'Operator', operator: ',', start: 6, end: 7 },\n            { type: 'WhiteSpace', start: 7, end: 8 },\n            { type: 'StringValue', value: 'red, black', quote: 'double', start: 8, end: 20 },\n            { type: 'Operator', operator: ',', start: 20, end: 21 },\n            { type: 'WhiteSpace', start: 21, end: 22 },\n            { type: 'Literal', value: 'rgb', start: 22, end: 25 },\n            { type: 'Bracket', open: true, start: 25, end: 26 },\n            { type: 'NumberValue', value: 0, rawValue: '0', unit: '', start: 26, end: 27 },\n            { type: 'Operator', operator: ',', start: 27, end: 28 },\n            { type: 'WhiteSpace', start: 28, end: 29 },\n            { type: 'NumberValue', value: 0, rawValue: '0', unit: '', start: 29, end: 30 },\n            { type: 'Operator', operator: ',', start: 30, end: 31 },\n            { type: 'WhiteSpace', start: 31, end: 32 },\n            { type: 'NumberValue', value: 0, rawValue: '0', unit: '', start: 32, end: 33 },\n            { type: 'Bracket', open: false, start: 33, end: 34 },\n            { type: 'WhiteSpace', start: 34, end: 35 },\n            { type: 'NumberValue', value: 10, rawValue: '10', unit: '%', start: 35, end: 38 },\n            { type: 'Bracket', open: false, start: 38, end: 39 }\n        ]);\n    });\n\n    it('important', () => {\n        deepEqual(tokenize('!'), [\n            { type: 'Operator', operator: '!', start: 0, end: 1 }\n        ]);\n\n        deepEqual(tokenize('p!'), [\n            { type: 'Literal', value: 'p', start: 0, end: 1 },\n            { type: 'Operator', operator: '!', start: 1, end: 2 }\n        ]);\n\n        deepEqual(tokenize('p10!'), [\n            { type: 'Literal', value: 'p', start: 0, end: 1 },\n            { type: 'NumberValue', value: 10, rawValue: '10', unit: '', start: 1, end: 3 },\n            { type: 'Operator', operator: '!', start: 3, end: 4 }\n        ]);\n    });\n\n    it('mixed', () => {\n        deepEqual(tokenize('bd1-s#fc0'), [\n            { type: 'Literal', value: 'bd', start: 0, end: 2 },\n            { type: 'NumberValue', value: 1, rawValue: '1', unit: '', start: 2, end: 3 },\n            { type: 'Operator', operator: '-', start: 3, end: 4 },\n            { type: 'Literal', value: 's', start: 4, end: 5 },\n            { type: 'ColorValue', r: 255, g: 204, b: 0, a: 1, raw: 'fc0', start: 5, end: 9 }\n        ]);\n\n        deepEqual(tokenize('bd#fc0-1'), [\n            { type: 'Literal', value: 'bd', start: 0, end: 2 },\n            { type: 'ColorValue', r: 255, g: 204, b: 0, a: 1, raw: 'fc0', start: 2, end: 6 },\n            { type: 'Operator', operator: '-', start: 6, end: 7 },\n            { type: 'NumberValue', value: 1, rawValue: '1', unit: '', start: 7, end: 8 }\n        ]);\n\n        deepEqual(tokenize('p0+m0'), [\n            { type: 'Literal', value: 'p', start: 0, end: 1 },\n            { type: 'NumberValue', value: 0, rawValue: '0', unit: '', start: 1, end: 2 },\n            { type: 'Operator', operator: '+', start: 2, end: 3 },\n            { type: 'Literal', value: 'm', start: 3, end: 4 },\n            { type: 'NumberValue', value: 0, rawValue: '0', unit: '', start: 4, end: 5 }\n        ]);\n\n        deepEqual(tokenize('p0!+m0!'), [\n            { type: 'Literal', value: 'p', start: 0, end: 1 },\n            { type: 'NumberValue', value: 0, rawValue: '0', unit: '', start: 1, end: 2 },\n            { type: 'Operator', operator: '!', start: 2, end: 3 },\n            { type: 'Operator', operator: '+', start: 3, end: 4 },\n            { type: 'Literal', value: 'm', start: 4, end: 5 },\n            { type: 'NumberValue', value: 0, rawValue: '0', unit: '', start: 5, end: 6 },\n            { type: 'Operator', operator: '!', start: 6, end: 7 }\n        ]);\n\n        deepEqual(tokenize('${2:0}%'), [\n            { type: 'Field', index: 2, name: '0', start: 0, end: 6 },\n            { type: 'Literal', value: '%', start: 6, end: 7 }\n        ]);\n\n        deepEqual(tokenize('.${1:5}'), [\n            { type: 'Literal', value: '.', start: 0, end: 1 },\n            { type: 'Field', index: 1, name: '5', start: 1, end: 7 },\n        ]);\n    });\n\n    it('embedded variables', () => {\n        deepEqual(tokenize('foo$bar'), [\n            { type: 'Literal', value: 'foo', start: 0, end: 3 },\n            { type: 'Literal', value: '$bar', start: 3, end: 7 }\n        ]);\n\n        deepEqual(tokenize('foo$bar-2'), [\n            { type: 'Literal', value: 'foo', start: 0, end: 3 },\n            { type: 'Literal', value: '$bar-2', start: 3, end: 9 }\n        ]);\n\n        deepEqual(tokenize('foo$bar@bam'), [\n            { type: 'Literal', value: 'foo', start: 0, end: 3 },\n            { type: 'Literal', value: '$bar', start: 3, end: 7 },\n            { type: 'Literal', value: '@bam', start: 7, end: 11 }\n        ]);\n\n        deepEqual(tokenize('@k10'), [\n            { type: 'Literal', value: '@k', start: 0, end: 2 },\n            { type: 'NumberValue', value: 10, rawValue: '10', unit: '', start: 2, end: 4 }\n        ]);\n    });\n});\n"
  },
  {
    "path": "packages/css-abbreviation/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig\",\n  \"compilerOptions\": {\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/scanner/.gitignore",
    "content": "/scanner.js\n/scanner.es.js\n/scanner.cjs\n/*.d.ts\n/*.map\n"
  },
  {
    "path": "packages/scanner/.npmignore",
    "content": "npm-debug.log*\nnode_modules\njspm_packages\n.npm\n/.*\n/*.*\n!/scanner.js\n!/scanner.cjs\n!/*.d.ts\n!/*.map\n/test\n/src\n"
  },
  {
    "path": "packages/scanner/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Sergey Chikuyonok <serge.che@gmail.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/scanner/package.json",
    "content": "{\n  \"name\": \"@emmetio/scanner\",\n  \"version\": \"1.0.4\",\n  \"description\": \"Scans given text character-by-character\",\n  \"main\": \"./dist/scanner.cjs\",\n  \"module\": \"./dist/scanner.js\",\n  \"types\": \"./dist/scanner.d.ts\",\n  \"type\": \"module\",\n  \"exports\": {\n    \"import\": \"./dist/scanner.js\",\n    \"require\": \"./dist/scanner.cjs\"\n  },\n  \"scripts\": {\n    \"test\": \"tsx --test ./test/*.ts\",\n    \"build\": \"rollup -c\",\n    \"watch\": \"rollup -wc\",\n    \"clean\": \"rm ./scanner.* ./*.d.ts\",\n    \"prepublishOnly\": \"npm test && npm run clean && npm run build\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/emmetio/stream-reader.git\"\n  },\n  \"keywords\": [\n    \"emmet\",\n    \"stream\",\n    \"scanner\"\n  ],\n  \"author\": \"Sergey Chikuyonok <serge.che@gmail.com>\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/emmetio/emmet/issues\"\n  },\n  \"homepage\": \"https://github.com/emmetio/emmet#readme\"\n}\n"
  },
  {
    "path": "packages/scanner/rollup.config.js",
    "content": "import typescript from '@rollup/plugin-typescript';\n\nexport default {\n    input: './src/scanner.ts',\n    plugins: [typescript()],\n    output: [{\n        format: 'cjs',\n        exports: 'named',\n        sourcemap: true,\n        file: './dist/scanner.cjs'\n    }, {\n        format: 'es',\n        sourcemap: true,\n        file: './dist/scanner.js'\n    }]\n};\n"
  },
  {
    "path": "packages/scanner/src/scanner.ts",
    "content": "export * from './utils';\n\ntype MatchFn = (ch: number) => boolean;\n\n/**\n * A streaming, character code-based string reader\n */\nexport default class Scanner {\n    /** Current string */\n    string: string;\n\n    /** Current scanner position */\n    pos: number;\n    /** Lower range limit where string reader is available */\n    start: number;\n\n    /** Upper range limit where string reader is available */\n    end: number;\n\n    constructor(str: string, start?: number, end?: number) {\n        if (end == null && typeof str === 'string') {\n            end = str.length;\n        }\n\n        this.string = str;\n        this.pos = this.start = start || 0;\n        this.end = end || 0;\n    }\n\n    /**\n     * Returns true only if the stream is at the end of the file.\n     */\n    eof(): boolean {\n        return this.pos >= this.end;\n    }\n\n    /**\n     * Creates a new stream instance which is limited to given `start` and `end`\n     * range. E.g. its `eof()` method will look at `end` property, not actual\n     * stream end\n     */\n    limit(start?: number, end?: number): Scanner {\n        return new Scanner(this.string, start, end);\n    }\n\n    /**\n     * Returns the next character code in the stream without advancing it.\n     * Will return NaN at the end of the file.\n     */\n    peek(): number {\n        return this.string.charCodeAt(this.pos);\n    }\n\n    /**\n     * Returns the next character in the stream and advances it.\n     * Also returns <code>undefined</code> when no more characters are available.\n     */\n    next(): number | undefined {\n        if (this.pos < this.string.length) {\n            return this.string.charCodeAt(this.pos++);\n        }\n    }\n\n    /**\n     * `match` can be a character code or a function that takes a character code\n     * and returns a boolean. If the next character in the stream 'matches'\n     * the given argument, it is consumed and returned.\n     * Otherwise, `false` is returned.\n     */\n    eat(match: number | MatchFn): boolean {\n        const ch = this.peek();\n        const ok = typeof match === 'function' ? match(ch) : ch === match;\n\n        if (ok) {\n            this.next();\n        }\n\n        return ok;\n    }\n\n    /**\n     * Repeatedly calls <code>eat</code> with the given argument, until it\n     * fails. Returns <code>true</code> if any characters were eaten.\n     */\n    eatWhile(match: number | MatchFn): boolean {\n        const start = this.pos;\n        while (!this.eof() && this.eat(match)) { /* */ }\n        return this.pos !== start;\n    }\n\n    /**\n     * Backs up the stream n characters. Backing it up further than the\n     * start of the current token will cause things to break, so be careful.\n     */\n    backUp(n: number) {\n        this.pos -= (n || 1);\n    }\n\n    /**\n     * Get the string between the start of the current token and the\n     * current stream position.\n     */\n    current(): string {\n        return this.substring(this.start, this.pos);\n    }\n\n    /**\n     * Returns substring for given range\n     */\n    substring(start: number, end?: number): string {\n        return this.string.slice(start, end);\n    }\n\n    /**\n     * Creates error object with current stream state\n     */\n    error(message: string, pos = this.pos): ScannerError {\n        return new ScannerError(`${message} at ${pos + 1}`, pos, this.string);\n    }\n}\n\nexport class ScannerError extends Error {\n    pos: number;\n    string: string;\n\n    constructor(message: string, pos: number, str: string) {\n        super(message);\n        this.pos = pos;\n        this.string = str;\n    }\n}\n"
  },
  {
    "path": "packages/scanner/src/utils.ts",
    "content": "import type Scanner from './scanner';\n\ninterface QuotedOptions {\n    /** A character code of quote-escape symbol */\n    escape?: number;\n\n    /** Throw error if quotes string can’t be properly consumed */\n    throws?: boolean;\n}\n\nconst defaultQuotedOptions: QuotedOptions = {\n    escape: 92,   // \\ character\n    throws: false\n};\n\n/**\n * Check if given code is a number\n */\nexport function isNumber(code: number): boolean {\n    return code > 47 && code < 58;\n}\n\n/**\n * Check if given character code is alpha code (letter through A to Z)\n */\nexport function isAlpha(code: number, from?: number, to?: number): boolean {\n    from = from || 65; // A\n    to = to || 90; // Z\n    code &= ~32; // quick hack to convert any char code to uppercase char code\n\n    return code >= from && code <= to;\n}\n\n/**\n * Check if given character code is alpha-numeric (letter through A to Z or number)\n */\nexport function isAlphaNumeric(code: number): boolean {\n    return isNumber(code) || isAlpha(code);\n}\n\nexport function isAlphaNumericWord(code: number): boolean {\n    return isNumber(code) || isAlphaWord(code);\n}\n\nexport function isAlphaWord(code: number): boolean {\n    return code === 95 /* _ */ || isAlpha(code);\n}\n\n/**\n * Check for Umlauts i.e. ä, Ä, ö, Ö, ü and Ü\n */\nexport function isUmlaut(code: number): boolean {\n    return code === 196\n        || code == 214\n        || code === 220\n        || code === 228\n        || code === 246\n        || code === 252;\n}\n\n/**\n * Check if given character code is a white-space character: a space character\n * or line breaks\n */\nexport function isWhiteSpace(code: number) {\n    return code === 32   /* space */\n        || code === 9    /* tab */\n        || code === 160; /* non-breaking space */\n}\n\n/**\n * Check if given character code is a space character\n */\nexport function isSpace(code: number): boolean {\n    return isWhiteSpace(code)\n        || code === 10  /* LF */\n        || code === 13; /* CR */\n}\n\n/**\n * Consumes 'single' or \"double\"-quoted string from given string, if possible\n * @return `true` if quoted string was consumed. The contents of quoted string\n * will be available as `stream.current()`\n */\nexport function eatQuoted(stream: Scanner, options?: QuotedOptions): boolean {\n    options = { ...defaultQuotedOptions, ...options };\n    const start = stream.pos;\n    const quote = stream.peek();\n\n    if (stream.eat(isQuote)) {\n        while (!stream.eof()) {\n            switch (stream.next()) {\n                case quote:\n                    stream.start = start;\n                    return true;\n\n                case options.escape:\n                    stream.next();\n                    break;\n            }\n        }\n\n        // If we’re here then stream wasn’t properly consumed.\n        // Revert stream and decide what to do\n        stream.pos = start;\n\n        if (options.throws) {\n            throw stream.error('Unable to consume quoted string');\n        }\n    }\n\n    return false;\n}\n\n/**\n * Check if given character code is a quote character\n */\nexport function isQuote(code: number): boolean {\n    return code === 39 /* ' */ || code === 34 /* \" */;\n}\n\n/**\n * Eats paired characters substring, for example `(foo)` or `[bar]`\n * @param open Character code of pair opening\n * @param close Character code of pair closing\n * @return Returns `true` if character pair was successfully consumed, it’s\n * content will be available as `stream.current()`\n */\nexport function eatPair(stream: Scanner, open: number, close: number, options?: QuotedOptions): boolean {\n    options = { ...defaultQuotedOptions, ...options };\n    const start = stream.pos;\n\n    if (stream.eat(open)) {\n        let stack = 1;\n        let ch: number;\n\n        while (!stream.eof()) {\n            if (eatQuoted(stream, options)) {\n                continue;\n            }\n\n            ch = stream.next()!;\n            if (ch === open) {\n                stack++;\n            } else if (ch === close) {\n                stack--;\n                if (!stack) {\n                    stream.start = start;\n                    return true;\n                }\n            } else if (ch === options.escape) {\n                stream.next();\n            }\n        }\n\n        // If we’re here then paired character can’t be consumed\n        stream.pos = start;\n\n        if (options.throws) {\n            throw stream.error(`Unable to find matching pair for ${String.fromCharCode(open)}`);\n        }\n    }\n\n    return false;\n}\n"
  },
  {
    "path": "packages/scanner/test/stream-reader.ts",
    "content": "import { describe, it } from 'node:test';\nimport { strictEqual as equal, ok } from 'node:assert';\nimport StreamReader from '../src/scanner';\n\ndescribe('Stream Reader', () => {\n    it('basic', () => {\n        const data = 'hello';\n        const s = new StreamReader(data);\n\n        equal(s.string, data);\n        equal(s.start, 0);\n        equal(s.pos, 0);\n\n        equal(s.peek(), data.charCodeAt(0));\n        equal(s.start, 0);\n        equal(s.pos, 0);\n\n        equal(s.next(), data.charCodeAt(0));\n        equal(s.next(), data.charCodeAt(1));\n        equal(s.start, 0);\n        equal(s.pos, 2);\n\n        equal(s.next(), data.charCodeAt(2));\n        equal(s.start, 0);\n        equal(s.pos, 3);\n\n        equal(s.current(), data.slice(0, 3));\n    });\n\n    it('should limit reader range', () => {\n        const outer = new StreamReader('foo bar baz');\n        const inner = outer.limit(4, 7);\n\n        ok(outer !== inner);\n\n        let outerValue = '';\n        let innerValue = '';\n\n        while (!outer.eof()) {\n            outerValue += String.fromCharCode(outer.next()!);\n        }\n\n        while (!inner.eof()) {\n            innerValue += String.fromCharCode(inner.next()!);\n        }\n\n        equal(outerValue, 'foo bar baz');\n        equal(innerValue, 'bar');\n    });\n});\n"
  },
  {
    "path": "packages/scanner/test/utils.ts",
    "content": "import { describe, it } from 'node:test';\nimport { strictEqual as equal, ok, throws } from 'node:assert';\nimport StreamReader from '../src/scanner';\nimport { eatPair, eatQuoted } from '../src/utils';\n\ndescribe('Pairs', () => {\n    const code = (ch: string) => ch.charCodeAt(0);\n\n    it('eat', () => {\n        const stream = new StreamReader('[foo] (bar (baz) bam)');\n\n        ok(eatPair(stream, code('['), code(']')));\n        equal(stream.start, 0);\n        equal(stream.pos, 5);\n        equal(stream.current(), '[foo]');\n\n        // No pair here\n        ok(!eatPair(stream, code('('), code(')'), { throws: true }));\n        stream.eatWhile(code(' '));\n\n        ok(eatPair(stream, code('('), code(')'), { throws: true }));\n        equal(stream.start, 6);\n        equal(stream.pos, 21);\n        equal(stream.current(), '(bar (baz) bam)');\n    });\n\n    it('eat with quotes', () => {\n        const stream = new StreamReader('[foo \"bar]\" ]');\n        ok(eatPair(stream, code('['), code(']')));\n        equal(stream.start, 0);\n        equal(stream.pos, 13);\n        equal(stream.current(), '[foo \"bar]\" ]');\n    });\n\n    it('handle invalid', () => {\n        const stream = new StreamReader('[foo');\n        ok(!eatPair(stream, code('['), code(']')));\n        equal(stream.start, 0);\n        equal(stream.pos, 0);\n\n        throws(() => ok(!eatPair(stream, code('['), code(']'), { throws: true })),\n            /Unable to find matching pair/);\n    });\n});\n\ndescribe('Quoted', () => {\n    it('eat quoted', () => {\n        const data = '\"foo\"   \\'bar\\'';\n        const stream = new StreamReader(data);\n\n        ok(eatQuoted(stream));\n        equal(stream.start, 0);\n        equal(stream.pos, 5);\n        equal(stream.current(), '\"foo\"');\n\n        // no double-quoted value ahead\n        ok(!eatQuoted(stream, { throws: true }));\n\n        // eat space\n        ok(stream.eatWhile(' '.charCodeAt(0)));\n        equal(stream.pos, 8);\n\n        ok(eatQuoted(stream));\n        equal(stream.start, 8);\n        equal(stream.pos, 13);\n        equal(stream.current(), '\\'bar\\'');\n        ok(stream.eof());\n    });\n\n    it('handle broken strings', () => {\n        const stream = new StreamReader('\"foo');\n        ok(!eatQuoted(stream));\n        equal(stream.pos, 0);\n\n        throws(() => eatQuoted(stream, { throws: true }), /Unable to consume quoted string/);\n    });\n\n    it('handle escapes', () => {\n        const stream = new StreamReader('\"foo\\\\\"bar\" baz');\n        ok(eatQuoted(stream));\n        equal(stream.start, 0);\n        equal(stream.pos, 10);\n        equal(stream.current(), '\"foo\\\\\"bar\"');\n    });\n});\n"
  },
  {
    "path": "packages/scanner/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig\",\n  \"compilerOptions\": {\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "rollup.config.js",
    "content": "import { extname } from 'path';\nimport typescript from '@rollup/plugin-typescript';\nimport nodeResolve from '@rollup/plugin-node-resolve';\n\nexport default {\n    input: './src/index.ts',\n    plugins: [nodeResolve(), json(), typescript()],\n    output: [{\n        file: './dist/emmet.js',\n        format: 'es',\n        sourcemap: true\n    }, {\n        file: './dist/emmet.cjs',\n        format: 'cjs',\n        exports: 'named',\n        sourcemap: true\n    }]\n};\n\nfunction json() {\n    return {\n        transform(code, id) {\n            if (extname(id) === '.json') {\n                return { code: `export default ${code}`, map: null };\n            }\n        }\n    };\n}\n"
  },
  {
    "path": "src/config.ts",
    "content": "import type { Abbreviation } from '@emmetio/abbreviation';\nimport markupSnippets from './snippets/html.json' with { type: 'json' };\nimport stylesheetSnippets from './snippets/css.json' with { type: 'json' };\nimport xslSnippets from './snippets/xsl.json' with { type: 'json' };\nimport pugSnippets from './snippets/pug.json' with { type: 'json' };\nimport variables from './snippets/variables.json' with { type: 'json' };\nimport type { CSSSnippet } from './stylesheet/snippets.js';\n\nexport type SyntaxType = 'markup' | 'stylesheet';\nexport type FieldOutput = (index: number, placeholder: string, offset: number, line: number, column: number) => string;\nexport type TextOutput = (text: string, offset: number, line: number, column: number) => string;\nexport type StringCase = '' | 'lower' | 'upper';\nexport interface SnippetsMap {\n    [name: string]: string;\n}\n\nexport interface AbbreviationContext {\n    name: string;\n    attributes?: { [name: string]: string | null };\n}\n\n/**\n * Raw config which contains per-syntax options. `markup` and `syntax` keys are\n * reserved for global settings for all markup and stylesheet syntaxes\n */\nexport interface GlobalConfig {\n    [syntax: string]: Partial<BaseConfig>;\n}\n\nexport interface BaseConfig {\n    /* Type of abbreviation context, default is `markup` */\n    type: SyntaxType;\n\n    /** Options for abbreviation output */\n    options: Partial<Options>;\n\n    /** Substitutions for variable names */\n    variables: SnippetsMap;\n\n    /** Abbreviation name to snippets mapping */\n    snippets: SnippetsMap;\n}\n\ninterface ResolvedConfig extends BaseConfig {\n    /** Host syntax */\n    syntax: string;\n\n    /**\n     * Context of abbreviation. For markup abbreviation, it contains parent tag\n     * name with attributes, for stylesheet abbreviation it contains property name\n     * if abbreviation is expanded as value\n     */\n    context?: AbbreviationContext;\n\n    /** Text to wrap with abbreviation */\n    text?: string | string[];\n\n    /** Max amount of repeated elements (fool proof) */\n    maxRepeat?: number;\n\n    /**\n     * Object for storing internal cache data to be shared across Emmet methods\n     * invocation. If provided, Emmet will store compute-intensive data in this\n     * object and will re-use it during editor session.\n     * Every time user settings are changed, you should empty cache by passing\n     * new object.\n     */\n    cache?: Cache;\n\n    /**\n     * A callback for internal warnings or errors (for example, when parsing invalid abbreviation)\n     */\n    warn?: (message: string, err?: Error) => void\n}\n\nexport type Config = ResolvedConfig & { options: Options };\nexport type UserConfig = Partial<ResolvedConfig>;\n\nexport interface Cache {\n    stylesheetSnippets?: CSSSnippet[];\n    markupSnippets?: { [name: string]: Abbreviation | null };\n}\n\nexport interface Options {\n    /////////////////////\n    // Generic options //\n    /////////////////////\n\n    /** A list of inline-level elements */\n    inlineElements: string[];\n\n    ////////////////////\n    // Output options //\n    ////////////////////\n\n    /** A string for one level indent */\n    'output.indent': string;\n\n    /**\n     * A string for base indent, e.g. context indentation which will be added\n     * for every generated line\n     */\n    'output.baseIndent': string;\n\n    /** A string to use as a new line */\n    'output.newline': string;\n\n    /** Tag case: lower, upper or '' (keep as-is) */\n    'output.tagCase': StringCase;\n\n    /** Attribute name case: lower, upper or '' (keep as-is) */\n    'output.attributeCase': StringCase;\n\n    /** Attribute value quotes: 'single' or 'double' */\n    'output.attributeQuotes': 'single' | 'double';\n\n    /** Enable output formatting (indentation and line breaks) */\n    'output.format': boolean;\n\n    /** When enabled, automatically adds inner line breaks for leaf (e.g. without children) nodes */\n    'output.formatLeafNode': boolean;\n\n    /** A list of tag names that should not get inner indentation */\n    'output.formatSkip': string[];\n\n    /** A list of tag names that should *always* get inner indentation. */\n    'output.formatForce': string[];\n\n    /**\n     * How many inline sibling elements should force line break for each tag.\n     * Set to `0` to output all inline elements without formatting.\n     * Set to `1` to output all inline elements with formatting (same as block-level).\n     */\n    'output.inlineBreak': number;\n\n    /**\n     * Produce compact notation of boolean attributes: attributes which doesn’t have value.\n     * With this option enabled, outputs `<div contenteditable>` instead of\n     * `<div contenteditable=\"contenteditable\">`\n     */\n    'output.compactBoolean': boolean;\n\n    /** A list of boolean attributes */\n    'output.booleanAttributes': string[];\n\n    /** Reverses attribute merging directions when resolving snippets */\n    'output.reverseAttributes': boolean;\n\n    /** Style of self-closing tags: html (`<br>`), xml (`<br/>`) or xhtml (`<br />`) */\n    'output.selfClosingStyle': 'html' | 'xml' | 'xhtml';\n\n    /**\n     * A function that takes field index and optional placeholder and returns\n     * a string field (tabstop) for host editor. For example, a TextMate-style\n     * field is `$index` or `${index:placeholder}`\n     * @param index Field index\n     * @param placeholder Field placeholder (default value), if any\n     * @param offset Current character offset from the beginning of generated content\n     * @param line Current line of generated output\n     * @param column Current column in line\n     */\n    'output.field': FieldOutput;\n\n    /**\n     * A function for processing text chunk passed to `OutputStream`.\n     * May be used by editor for escaping characters, if necessary\n     */\n    'output.text': TextOutput;\n\n    ////////////////////\n    // Markup options //\n    ////////////////////\n\n    /**\n     * Automatically update value of <a> element's href attribute\n     * if inserting URL or email\n     */\n    'markup.href': boolean;\n\n    /**\n     * Attribute name mapping. Can be used to change attribute names for output.\n     * For example, `class` -> `className` in JSX. If a key ends with `*`, this\n     * value will be used for multi-attributes: currentry, it’s a `class` and `id`\n     * since `multiple` marker is added for shorthand attributes only.\n     * Example: `{ \"class*\": \"styleName\" }`\n     */\n    'markup.attributes'?: Record<string, string>;\n\n    /**\n     * Prefixes for attribute values.\n     * If specified, a value is treated as prefix for object notation and\n     * automatically converts attribute value into expression if `jsx` is enabled.\n     * Same as in `markup.attributes` option, a `*` can be used.\n     */\n    'markup.valuePrefix'?: Record<string, string>;\n\n    ////////////////////////////////\n    // Element commenting options //\n    ////////////////////////////////\n\n    /**\n     * Enable/disable element commenting: generate comments before open and/or\n     * after close tag\n     */\n    'comment.enabled': boolean;\n\n    /**\n     * Attributes that should trigger node commenting on specific node,\n     * if commenting is enabled\n     */\n    'comment.trigger': string[];\n\n    /**\n     * Template string for comment to be placed *before* opening tag\n     */\n    'comment.before': string;\n\n    /**\n     * Template string for comment to be placed *after* closing tag.\n     * Example: `\\n<!-- /[#ID][.CLASS] -->`\n     */\n    'comment.after': string;\n\n    /////////////////\n    // BEM options //\n    /////////////////\n\n    /** Enable/disable BEM addon */\n    'bem.enabled': boolean;\n\n    /** A string for separating elements in output class */\n    'bem.element': string;\n\n    /** A string for separating modifiers in output class */\n    'bem.modifier': string;\n\n    /////////////////\n    // JSX options //\n    /////////////////\n\n    /** Enable/disable JSX addon */\n    'jsx.enabled': boolean;\n\n    ////////////////////////\n    // Stylesheet options //\n    ////////////////////////\n\n    /** List of globally available keywords for properties */\n    'stylesheet.keywords': string[];\n\n    /**\n     * List of unitless properties, e.g. properties where numeric values without\n     * explicit unit will be outputted as is, without default value\n     */\n    'stylesheet.unitless': string[];\n\n    /** Use short hex notation where possible, e.g. `#000` instead of `#000000` */\n    'stylesheet.shortHex': boolean;\n\n    /** A string between property name and value */\n    'stylesheet.between': string;\n\n    /** A string after property value */\n    'stylesheet.after': string;\n\n    /** A unit suffix to output by default after integer values, 'px' by default */\n    'stylesheet.intUnit': string;\n\n    /** A unit suffix to output by default after float values, 'em' by default */\n    'stylesheet.floatUnit': string;\n\n    /**\n     * Aliases for custom units in abbreviation. For example, `r: 'rem'` will\n     * output `10rem` for abbreviation `10r`\n     */\n    'stylesheet.unitAliases': SnippetsMap;\n\n    /** Output abbreviation as JSON object properties (for CSS-in-JS syntaxes) */\n    'stylesheet.json': boolean;\n\n    /** Use double quotes for JSON values */\n    'stylesheet.jsonDoubleQuotes': boolean;\n\n    /**\n     * A float number between 0 and 1 to pick fuzzy-matched abbreviations.\n     * Lower value will pick more abbreviations (and less accurate)\n     */\n    'stylesheet.fuzzySearchMinScore': number;\n\n    /**\n     * Force strict abbreviation match. If Emmet is unable to match abbreviation\n     * with existing snippets, it will convert it to CSS property (`false`)\n     * or skip it (`true`). E.g. `foo-bar` will expand to `foo: bar` if this option\n     * is disabled or empty string if enabled\n     */\n    'stylesheet.strictMatch': boolean;\n}\n\n/**\n * Default syntaxes for abbreviation types\n */\nexport const defaultSyntaxes: { [name in SyntaxType]: string } = {\n    markup: 'html',\n    stylesheet: 'css'\n};\n\n/**\n * List of all known syntaxes\n */\nexport const syntaxes = {\n    markup: ['html', 'xml', 'xsl', 'jsx', 'js', 'pug', 'slim', 'haml', 'vue', 'svelte'],\n    stylesheet: ['css', 'sass', 'scss', 'less', 'sss', 'stylus']\n};\n\nexport const defaultOptions: Options = {\n    'inlineElements': [\n        'a', 'abbr', 'acronym', 'applet', 'b', 'basefont', 'bdo',\n        'big', 'br', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i',\n        'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'map', 'object', 'q',\n        's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup',\n        'textarea', 'tt', 'u', 'var'\n    ],\n    'output.indent': '\\t',\n    'output.baseIndent': '',\n    'output.newline': '\\n',\n    'output.tagCase': '',\n    'output.attributeCase': '',\n    'output.attributeQuotes': 'double',\n    'output.format': true,\n    'output.formatLeafNode': false,\n    'output.formatSkip': ['html'],\n    'output.formatForce': ['body'],\n    'output.inlineBreak': 3,\n    'output.compactBoolean': false,\n    'output.booleanAttributes': [\n        'contenteditable', 'seamless', 'async', 'autofocus',\n        'autoplay', 'checked', 'controls', 'defer', 'disabled', 'formnovalidate',\n        'hidden', 'ismap', 'loop', 'multiple', 'muted', 'novalidate', 'readonly',\n        'required', 'reversed', 'selected', 'typemustmatch'\n    ],\n    'output.reverseAttributes': false,\n    'output.selfClosingStyle': 'html',\n    'output.field': (index, placeholder) => placeholder,\n    'output.text': text => text,\n\n    'markup.href': true,\n\n    'comment.enabled': false,\n    'comment.trigger': ['id', 'class'],\n    'comment.before': '',\n    'comment.after': '\\n<!-- /[#ID][.CLASS] -->',\n\n    'bem.enabled': false,\n    'bem.element': '__',\n    'bem.modifier': '_',\n\n    'jsx.enabled': false,\n\n    'stylesheet.keywords': ['auto', 'inherit', 'unset', 'none'],\n    'stylesheet.unitless': ['z-index', 'line-height', 'opacity', 'font-weight', 'zoom', 'flex', 'flex-grow', 'flex-shrink'],\n    'stylesheet.shortHex': true,\n    'stylesheet.between': ': ',\n    'stylesheet.after': ';',\n    'stylesheet.intUnit': 'px',\n    'stylesheet.floatUnit': 'em',\n    'stylesheet.unitAliases': { e: 'em', p: '%', x: 'ex', r: 'rem' },\n    'stylesheet.json': false,\n    'stylesheet.jsonDoubleQuotes': false,\n    'stylesheet.fuzzySearchMinScore': 0,\n    'stylesheet.strictMatch': false\n};\n\nexport const defaultConfig: Config = {\n    type: 'markup',\n    syntax: 'html',\n    variables,\n    snippets: {},\n    options: defaultOptions\n};\n\n/**\n * Default per-syntax config\n */\nexport const syntaxConfig: GlobalConfig = {\n    markup: {\n        snippets: parseSnippets(markupSnippets),\n    },\n    xhtml: {\n        options: {\n            'output.selfClosingStyle': 'xhtml'\n        }\n    },\n    xml: {\n        options: {\n            'output.selfClosingStyle': 'xml'\n        }\n    },\n    xsl: {\n        snippets: parseSnippets(xslSnippets),\n        options: {\n            'output.selfClosingStyle': 'xml'\n        }\n    },\n    jsx: {\n        options: {\n            'jsx.enabled': true,\n            'markup.attributes': {\n                'class': 'className',\n                'class*': 'styleName',\n                'for': 'htmlFor'\n            },\n            'markup.valuePrefix': {\n                'class*': 'styles'\n            }\n        }\n    },\n    vue: {\n        options: {\n            'markup.attributes': {\n                'class*': ':class',\n            }\n        }\n    },\n    svelte: {\n        options: {\n            'jsx.enabled': true\n        }\n    },\n    pug: {\n        snippets: parseSnippets(pugSnippets)\n    },\n\n    stylesheet: {\n        snippets: parseSnippets(stylesheetSnippets)\n    },\n\n    sass: {\n        options: {\n            'stylesheet.after': ''\n        }\n    },\n    stylus: {\n        options: {\n            'stylesheet.between': ' ',\n            'stylesheet.after': '',\n        }\n    }\n};\n\n/**\n * Parses raw snippets definitions with possibly multiple keys into a plan\n * snippet map\n */\nexport function parseSnippets(snippets: SnippetsMap): SnippetsMap {\n    const result: SnippetsMap = {};\n    Object.keys(snippets).forEach(k => {\n        for (const name of k.split('|')) {\n            result[name] = snippets[k];\n        }\n    });\n\n    return result;\n}\n\nexport default function resolveConfig(config: UserConfig = {}, globals: GlobalConfig = {}): Config {\n    const type: SyntaxType = config.type || 'markup';\n    const syntax: string = config.syntax || defaultSyntaxes[type];\n\n    return {\n        ...defaultConfig,\n        ...config,\n        type,\n        syntax,\n        variables: mergedData(type, syntax, 'variables', config, globals),\n        snippets: mergedData(type, syntax, 'snippets', config, globals),\n        options: mergedData(type, syntax, 'options', config, globals)\n    };\n}\n\nfunction mergedData<K extends keyof BaseConfig>(type: SyntaxType, syntax: string, key: K, config: UserConfig, globals: GlobalConfig = {}): Config[K] {\n    const typeDefaults = syntaxConfig[type];\n    const typeOverride = globals[type];\n    const syntaxDefaults = syntaxConfig[syntax];\n    const syntaxOverride = globals[syntax];\n\n    return {\n        ...(defaultConfig[key] as object),\n        ...(typeDefaults && typeDefaults[key] as object),\n        ...(syntaxDefaults && syntaxDefaults[key] as object),\n        ...(typeOverride && typeOverride[key] as object),\n        ...(syntaxOverride && syntaxOverride[key] as object),\n        ...(config[key] as object)\n    } as Config[K];\n}\n"
  },
  {
    "path": "src/extract-abbreviation/brackets.ts",
    "content": "export const enum Brackets {\n    SquareL = 91,\n    SquareR = 93,\n    RoundL = 40,\n    RoundR = 41,\n    CurlyL = 123,\n    CurlyR = 125,\n}\n\nexport const bracePairs = {\n    [Brackets.SquareL]: Brackets.SquareR,\n    [Brackets.RoundL]: Brackets.RoundR,\n    [Brackets.CurlyL]: Brackets.CurlyR,\n};\n"
  },
  {
    "path": "src/extract-abbreviation/index.ts",
    "content": "import type { SyntaxType } from '../config';\nimport backwardScanner, { sol, peek, consume, type BackwardScanner } from './reader';\nimport isAtHTMLTag from './is-html';\nimport { isQuote } from './quotes';\nimport { Brackets, bracePairs } from './brackets';\n\nexport interface ExtractOptions {\n    /**\n     * Allow parser to look ahead of `pos` index for searching of missing\n     * abbreviation parts. Most editors automatically inserts closing braces for\n     * `[`, `{` and `(`, which will most likely be right after current caret position.\n     * So in order to properly expand abbreviation, user must explicitly move\n     * caret right after auto-inserted braces. With this option enabled, parser\n     * will search for closing braces right after `pos`. Default is `true`\n     */\n    lookAhead: boolean;\n\n    /**\n     * Type of context syntax of expanded abbreviation.\n     * In 'stylesheet' syntax, brackets `[]` and `{}` are not supported thus\n     * not extracted.\n     */\n    type: SyntaxType;\n\n    /**\n     * A string that should precede abbreviation in order to make it successfully\n     * extracted. If given, the abbreviation will be extracted from the nearest\n     * `prefix` occurrence.\n     */\n    prefix: string;\n}\n\nexport interface ExtractedAbbreviation {\n    /** Extracted abbreviation */\n    abbreviation: string;\n\n    /** Location of abbreviation in input string */\n    location: number;\n\n    /** Start location of matched abbreviation, including prefix */\n    start: number;\n\n    /** End location of extracted abbreviation */\n    end: number;\n}\n\nconst code = (ch: string) => ch.charCodeAt(0);\nconst specialChars = '#.*:$-_!@%^+>/'.split('').map(code);\n\nconst defaultOptions: ExtractOptions = {\n    type: 'markup',\n    lookAhead: true,\n    prefix: ''\n};\n\n/**\n * Extracts Emmet abbreviation from given string.\n * The goal of this module is to extract abbreviation from current editor’s line,\n * e.g. like this: `<span>.foo[title=bar|]</span>` -> `.foo[title=bar]`, where\n * `|` is a current caret position.\n * @param line A text line where abbreviation should be expanded\n * @param pos Caret position in line. If not given, uses end of line\n * @param options Extracting options\n */\nexport default function extractAbbreviation(line: string, pos: number = line.length, options: Partial<ExtractOptions> = {}): ExtractedAbbreviation | undefined {\n    // make sure `pos` is within line range\n    const opt: ExtractOptions = { ...defaultOptions, ...options };\n    pos = Math.min(line.length, Math.max(0, pos == null ? line.length : pos));\n\n    if (opt.lookAhead) {\n        pos = offsetPastAutoClosed(line, pos, opt);\n    }\n\n    let ch: number;\n    const start = getStartOffset(line, pos, opt.prefix || '');\n    if (start === -1) {\n        return void 0;\n    }\n\n    const scanner = backwardScanner(line, start);\n    scanner.pos = pos;\n    const stack: number[] = [];\n\n    while (!sol(scanner)) {\n        ch = peek(scanner);\n\n        if (stack.includes(Brackets.CurlyR)) {\n            if (ch === Brackets.CurlyR) {\n                stack.push(ch);\n                scanner.pos--;\n                continue;\n            }\n\n            if (ch !== Brackets.CurlyL) {\n                scanner.pos--;\n                continue;\n            }\n        }\n\n        if (isCloseBrace(ch, opt.type)) {\n            stack.push(ch);\n        } else if (isOpenBrace(ch, opt.type)) {\n            if (stack.pop() !== bracePairs[ch]) {\n                // unexpected brace\n                break;\n            }\n        } else if (stack.includes(Brackets.SquareR) || stack.includes(Brackets.CurlyR)) {\n            // respect all characters inside attribute sets or text nodes\n            scanner.pos--;\n            continue;\n        } else if (isAtHTMLTag(scanner) || !isAbbreviation(ch)) {\n            break;\n        }\n\n        scanner.pos--;\n    }\n\n    if (!stack.length && scanner.pos !== pos) {\n        // Found something, remove some invalid symbols from the\n        // beginning and return abbreviation\n        const abbreviation = line.slice(scanner.pos, pos).replace(/^[*+>^]+/, '');\n        return {\n            abbreviation,\n            location: pos - abbreviation.length,\n            start: options.prefix\n                ? start - options.prefix.length\n                : pos - abbreviation.length,\n            end: pos\n        };\n    }\n}\n\n/**\n * Returns new `line` index which is right after characters beyound `pos` that\n * editor will likely automatically close, e.g. }, ], and quotes\n */\nfunction offsetPastAutoClosed(line: string, pos: number, options: ExtractOptions): number {\n    // closing quote is allowed only as a next character\n    if (isQuote(line.charCodeAt(pos))) {\n        pos++;\n    }\n\n    // offset pointer until non-autoclosed character is found\n    while (isCloseBrace(line.charCodeAt(pos), options.type)) {\n        pos++;\n    }\n\n    return pos;\n}\n\n/**\n * Returns start offset (left limit) in `line` where we should stop looking for\n * abbreviation: it’s nearest to `pos` location of `prefix` token\n */\nfunction getStartOffset(line: string, pos: number, prefix: string): number {\n    if (!prefix) {\n        return 0;\n    }\n\n    const scanner = backwardScanner(line);\n    const compiledPrefix = prefix.split('').map(code);\n    scanner.pos = pos;\n    let result: number;\n\n    while (!sol(scanner)) {\n        if (consumePair(scanner, Brackets.SquareR, Brackets.SquareL) || consumePair(scanner, Brackets.CurlyR, Brackets.CurlyL)) {\n            continue;\n        }\n\n        result = scanner.pos;\n        if (consumeArray(scanner, compiledPrefix)) {\n            return result;\n        }\n\n        scanner.pos--;\n    }\n\n    return -1;\n}\n\n/**\n * Consumes full character pair, if possible\n */\nfunction consumePair(scanner: BackwardScanner, close: number, open: number): boolean {\n    const start = scanner.pos;\n    if (consume(scanner, close)) {\n        while (!sol(scanner)) {\n            if (consume(scanner, open)) {\n                return true;\n            }\n\n            scanner.pos--;\n        }\n    }\n\n    scanner.pos = start;\n    return false;\n}\n\n/**\n * Consumes all character codes from given array, right-to-left, if possible\n */\nfunction consumeArray(scanner: BackwardScanner, arr: number[]) {\n    const start = scanner.pos;\n    let consumed = false;\n\n    for (let i = arr.length - 1; i >= 0 && !sol(scanner); i--) {\n        if (!consume(scanner, arr[i])) {\n            break;\n        }\n\n        consumed = i === 0;\n    }\n\n    if (!consumed) {\n        scanner.pos = start;\n    }\n\n    return consumed;\n}\n\nfunction isAbbreviation(ch: number) {\n    return (ch > 64 && ch < 91)   // uppercase letter\n        || (ch > 96 && ch < 123)  // lowercase letter\n        || (ch > 47 && ch < 58)   // number\n        || specialChars.includes(ch); // special character\n}\n\nfunction isOpenBrace(ch: number, syntax: SyntaxType) {\n    return ch === Brackets.RoundL || (syntax === 'markup' && (ch === Brackets.SquareL || ch === Brackets.CurlyL));\n}\n\nfunction isCloseBrace(ch: number, syntax: SyntaxType) {\n    return ch === Brackets.RoundR || (syntax === 'markup' && (ch === Brackets.SquareR || ch === Brackets.CurlyR));\n}\n"
  },
  {
    "path": "src/extract-abbreviation/is-html.ts",
    "content": "import { isQuote, consumeQuoted } from './quotes';\nimport { type BackwardScanner, consume, sol, consumeWhile, peek } from './reader';\nimport { Brackets, bracePairs } from './brackets';\n\nconst enum Chars {\n    Tab = 9,\n    Space = 32,\n    /** `-` character */\n    Dash = 45,\n    /** `/` character */\n    Slash = 47,\n    /** `:` character */\n    Colon = 58,\n    /** `=` character */\n    Equals = 61,\n    /** `<` character */\n    AngleLeft = 60,\n    /** `>` character */\n    AngleRight = 62,\n}\n\n/**\n * Check if given reader’s current position points at the end of HTML tag\n */\nexport default function isHtml(scanner: BackwardScanner): boolean {\n    const start = scanner.pos;\n\n    if (!consume(scanner, Chars.AngleRight)) {\n        return false;\n    }\n\n    let ok = false;\n    consume(scanner, Chars.Slash); // possibly self-closed element\n\n    while (!sol(scanner)) {\n        consumeWhile(scanner, isWhiteSpace);\n\n        if (consumeIdent(scanner)) {\n            // ate identifier: could be a tag name, boolean attribute or unquoted\n            // attribute value\n            if (consume(scanner, Chars.Slash)) {\n                // either closing tag or invalid tag\n                ok = consume(scanner, Chars.AngleLeft);\n                break;\n            } else if (consume(scanner, Chars.AngleLeft)) {\n                // opening tag\n                ok = true;\n                break;\n            } else if (consume(scanner, isWhiteSpace)) {\n                // boolean attribute\n                continue;\n            } else if (consume(scanner, Chars.Equals)) {\n                // simple unquoted value or invalid attribute\n                if (consumeIdent(scanner)) {\n                    continue;\n                }\n                break;\n            } else if (consumeAttributeWithUnquotedValue(scanner)) {\n                // identifier was a part of unquoted value\n                ok = true;\n                break;\n            }\n\n            // invalid tag\n            break;\n        }\n\n        if (consumeAttribute(scanner)) {\n            continue;\n        }\n\n        break;\n    }\n\n    scanner.pos = start;\n    return ok;\n}\n\n/**\n * Consumes HTML attribute from given string.\n * @return `true` if attribute was consumed.\n */\nfunction consumeAttribute(scanner: BackwardScanner): boolean {\n    return consumeAttributeWithQuotedValue(scanner) || consumeAttributeWithUnquotedValue(scanner);\n}\n\nfunction consumeAttributeWithQuotedValue(scanner: BackwardScanner): boolean {\n    const start = scanner.pos;\n    if (consumeQuoted(scanner) && consume(scanner, Chars.Equals) && consumeIdent(scanner)) {\n        return true;\n    }\n\n    scanner.pos = start;\n    return false;\n}\n\nfunction consumeAttributeWithUnquotedValue(scanner: BackwardScanner): boolean {\n    const start = scanner.pos;\n    const stack: Brackets[] = [];\n    while (!sol(scanner)) {\n        const ch = peek(scanner);\n        if (isCloseBracket(ch)) {\n            stack.push(ch);\n        } else if (isOpenBracket(ch)) {\n            if (stack.pop() !== bracePairs[ch]) {\n                // Unexpected open bracket\n                break;\n            }\n        } else if (!isUnquotedValue(ch)) {\n            break;\n        }\n        scanner.pos--;\n    }\n\n    if (start !== scanner.pos && consume(scanner, Chars.Equals) && consumeIdent(scanner)) {\n        return true;\n    }\n\n    scanner.pos = start;\n    return false;\n}\n\n/**\n * Consumes HTML identifier from stream\n */\nfunction consumeIdent(scanner: BackwardScanner): boolean {\n    return consumeWhile(scanner, isIdent);\n}\n\n/**\n * Check if given character code belongs to HTML identifier\n */\nfunction isIdent(ch: number): boolean {\n    return ch === Chars.Colon || ch === Chars.Dash || isAlpha(ch) || isNumber(ch);\n}\n\n/**\n * Check if given character code is alpha code (letter though A to Z)\n */\nfunction isAlpha(ch: number): boolean {\n    ch &= ~32; // quick hack to convert any char code to uppercase char code\n    return ch >= 65 && ch <= 90; // A-Z\n}\n\n/**\n * Check if given code is a number\n */\nfunction isNumber(ch: number): boolean {\n    return ch > 47 && ch < 58;\n}\n\n/**\n * Check if given code is a whitespace\n */\nfunction isWhiteSpace(ch: number): boolean {\n    return ch === Chars.Space || ch === Chars.Tab;\n}\n\n/**\n * Check if given code may belong to unquoted attribute value\n */\nfunction isUnquotedValue(ch: number): boolean {\n    return !isNaN(ch) && ch !== Chars.Equals && !isWhiteSpace(ch) && !isQuote(ch);\n}\n\nfunction isOpenBracket(ch: number): boolean {\n    return ch === Brackets.CurlyL || ch === Brackets.RoundL || ch === Brackets.SquareL;\n}\n\nfunction isCloseBracket(ch: number): boolean {\n    return ch === Brackets.CurlyR || ch === Brackets.RoundR || ch === Brackets.SquareR;\n}\n"
  },
  {
    "path": "src/extract-abbreviation/quotes.ts",
    "content": "import { type BackwardScanner, previous, sol, peek } from './reader.js';\n\nconst enum Chars {\n    SingleQuote = 39,\n    DoubleQuote = 34,\n    Escape = 92\n}\n\n/**\n * Check if given character code is a quote\n */\nexport function isQuote(c?: number) {\n    return c === Chars.SingleQuote || c === Chars.DoubleQuote;\n}\n\n/**\n * Consumes quoted value, if possible\n * @return Returns `true` is value was consumed\n */\nexport function consumeQuoted(scanner: BackwardScanner): boolean {\n    const start = scanner.pos;\n    const quote = previous(scanner);\n\n    if (isQuote(quote)) {\n        while (!sol(scanner)) {\n            if (previous(scanner) === quote && peek(scanner) !== Chars.Escape) {\n                return true;\n            }\n        }\n    }\n\n    scanner.pos = start;\n    return false;\n}\n"
  },
  {
    "path": "src/extract-abbreviation/reader.ts",
    "content": "type Match = ((code: number) => boolean) | number;\n\nexport interface BackwardScanner {\n    /** Text to scan */\n    text: string;\n\n    /** Left bound till given text must be scanned */\n    start: number;\n\n    /** Current scanner position */\n    pos: number;\n}\n\n/**\n * Creates structure for scanning given string in backward direction\n */\nexport default function backwardScanner(text: string, start = 0): BackwardScanner {\n    return { text, start, pos: text.length };\n}\n\n/**\n * Check if given scanner position is at start of scanned text\n */\nexport function sol(scanner: BackwardScanner) {\n    return scanner.pos === scanner.start;\n}\n\n/**\n * “Peeks” character code an current scanner location without advancing it\n */\nexport function peek(scanner: BackwardScanner, offset = 0) {\n    return scanner.text.charCodeAt(scanner.pos - 1 + offset);\n}\n\n/**\n * Returns current character code and moves character location one symbol back\n */\nexport function previous(scanner: BackwardScanner) {\n    if (!sol(scanner)) {\n        return scanner.text.charCodeAt(--scanner.pos);\n    }\n}\n\n/**\n * Consumes current character code if it matches given `match` code or function\n */\nexport function consume(scanner: BackwardScanner, match: Match): boolean {\n    if (sol(scanner)) {\n        return false;\n    }\n\n    const ok = typeof match === 'function'\n        ? match(peek(scanner))\n        : match === peek(scanner);\n\n    if (ok) {\n        scanner.pos--;\n    }\n\n    return !!ok;\n}\n\nexport function consumeWhile(scanner: BackwardScanner, match: Match): boolean {\n    const start = scanner.pos;\n    while (consume(scanner, match)) {\n        // empty\n    }\n    return scanner.pos < start;\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "import markupAbbreviation, { type Abbreviation } from '@emmetio/abbreviation';\nimport stylesheetAbbreviation, { type CSSAbbreviation } from '@emmetio/css-abbreviation';\nimport parseMarkup, { stringify as stringifyMarkup } from './markup';\nimport parseStylesheet, {\n    stringify as stringifyStylesheet,\n    convertSnippets as parseStylesheetSnippets,\n    CSSAbbreviationScope\n} from './stylesheet';\nimport resolveConfig, { type UserConfig, type Config } from './config';\n\nexport default function expandAbbreviation(abbr: string, config?: UserConfig): string {\n    const resolvedConfig = resolveConfig(config);\n    return resolvedConfig.type === 'stylesheet'\n        ? stylesheet(abbr, resolvedConfig)\n        : markup(abbr, resolvedConfig);\n}\n\n/**\n * Expands given *markup* abbreviation (e.g. regular Emmet abbreviation that\n * produces structured output like HTML) and outputs it according to options\n * provided in config\n */\nexport function markup(abbr: string | Abbreviation, config: Config) {\n    return stringifyMarkup(parseMarkup(abbr, config), config);\n}\n\n/**\n * Expands given *stylesheet* abbreviation (a special Emmet abbreviation designed for\n * stylesheet languages like CSS, SASS etc.) and outputs it according to options\n * provided in config\n */\nexport function stylesheet(abbr: string | CSSAbbreviation, config: Config) {\n    return stringifyStylesheet(parseStylesheet(abbr, config), config);\n}\n\nexport {\n    markupAbbreviation, parseMarkup, stringifyMarkup,\n    stylesheetAbbreviation, parseStylesheet, stringifyStylesheet, parseStylesheetSnippets,\n    CSSAbbreviationScope\n};\nexport type {\n    Abbreviation as MarkupAbbreviation, CSSAbbreviation as StylesheetAbbreviation,\n};\nexport { default as extract, type ExtractOptions, type ExtractedAbbreviation } from './extract-abbreviation';\nexport { default as resolveConfig } from './config';\nexport type { GlobalConfig, SyntaxType, Config, UserConfig, Options, AbbreviationContext} from './config';\n"
  },
  {
    "path": "src/markup/addon/bem.ts",
    "content": "import type { AbbreviationNode, Value } from '@emmetio/abbreviation';\nimport type { Container } from '../utils';\nimport type { Config, AbbreviationContext } from '../../config';\n\ninterface BEMAbbreviationNode extends AbbreviationNode {\n    _bem?: BEMData;\n}\n\ninterface BEMAbbreviationContext extends AbbreviationContext {\n    _bem?: BEMData;\n}\n\ninterface BEMData {\n    classNames: string[];\n    block?: string ;\n}\n\nconst reElement = /^(-+)([a-z0-9]+[a-z0-9-]*)/i;\nconst reModifier = /^(_+)([a-z0-9]+[a-z0-9-_]*)/i;\nconst blockCandidates1 = (className: string) => /^[a-z]\\-/i.test(className);\nconst blockCandidates2 = (className: string) => /^[a-z]/i.test(className);\n\nexport default function bem(node: AbbreviationNode, ancestors: Container[], config: Config) {\n    expandClassNames(node);\n    expandShortNotation(node, ancestors, config);\n}\n\n/**\n * Expands existing class names in BEM notation in given `node`.\n * For example, if node contains `b__el_mod` class name, this method ensures\n * that element contains `b__el` class as well\n */\nfunction expandClassNames(node: BEMAbbreviationNode) {\n    const data = getBEMData(node);\n\n    const classNames: string[] = [];\n    for (const cl of data.classNames) {\n        // remove all modifiers and element prefixes from class name to get a base element name\n        const ix = cl.indexOf('_');\n        if (ix > 0 && !cl.startsWith('-')) {\n            classNames.push(cl.slice(0, ix));\n            classNames.push(cl.slice(ix));\n        } else {\n            classNames.push(cl);\n        }\n    }\n\n    if (classNames.length) {\n        data.classNames = classNames.filter(uniqueClass);\n        data.block = findBlockName(data.classNames);\n        updateClass(node, data.classNames.join(' '));\n    }\n}\n\n/**\n * Expands short BEM notation, e.g. `-element` and `_modifier`\n */\nfunction expandShortNotation(node: BEMAbbreviationNode, ancestors: Container[], config: Config) {\n    const data = getBEMData(node);\n    const classNames: string[] = [];\n    const { options } = config;\n    const path = ancestors.slice(1).concat(node) as BEMAbbreviationNode[];\n\n    for (let cl of data.classNames) {\n        let prefix: string = '';\n        let m: RegExpMatchArray | null;\n        const originalClass = cl;\n\n        // parse element definition (could be only one)\n        if (m = cl.match(reElement)) {\n            prefix = getBlockName(path, m[1].length, config.context) + options['bem.element'] + m[2];\n            classNames.push(prefix);\n            cl = cl.slice(m[0].length);\n        }\n\n        // parse modifiers definitions\n        if (m = cl.match(reModifier)) {\n            if (!prefix) {\n                prefix = getBlockName(path, m[1].length);\n                classNames.push(prefix);\n            }\n\n            classNames.push(`${prefix}${options['bem.modifier']}${m[2]}`);\n            cl = cl.slice(m[0].length);\n        }\n\n        if (cl === originalClass) {\n            // class name wasn’t modified: it’s not a BEM-specific class,\n            // add it as-is into output\n            classNames.push(originalClass);\n        }\n    }\n\n    const arrClassNames = classNames.filter(uniqueClass);\n    if (arrClassNames.length) {\n        updateClass(node, arrClassNames.join(' '));\n    }\n}\n\n/**\n * Returns BEM data from given abbreviation node\n */\nfunction getBEMData(node: BEMAbbreviationNode): BEMData {\n    if (!node._bem) {\n        let classValue = '';\n        if (node.attributes) {\n            for (const attr of node.attributes) {\n                if (attr.name === 'class' && attr.value) {\n                    classValue = stringifyValue(attr.value);\n                    break;\n                }\n            }\n        }\n\n        node._bem = parseBEM(classValue);\n    }\n\n    return node._bem;\n}\n\nfunction getBEMDataFromContext(context: BEMAbbreviationContext) {\n    if (!context._bem) {\n        context._bem = parseBEM(context.attributes && context.attributes.class || '');\n    }\n\n    return context._bem;\n}\n\n/**\n * Parses BEM data from given class name\n */\nfunction parseBEM(classValue?: string): BEMData {\n    const classNames = classValue ? classValue.split(/\\s+/) : [];\n    return {\n        classNames,\n        block: findBlockName(classNames)\n    };\n}\n\n/**\n * Returns block name for given `node` by `prefix`, which tells the depth of\n * of parent node lookup\n */\nfunction getBlockName(ancestors: BEMAbbreviationNode[], depth: number = 0, context?: BEMAbbreviationContext): string {\n    const maxParentIx = 0;\n    let parentIx = Math.max(ancestors.length - depth, maxParentIx);\n    do {\n        const parent = ancestors[parentIx];\n        if (parent) {\n            const data = getBEMData(parent as BEMAbbreviationNode);\n            if (data.block) {\n                return data.block;\n            }\n        }\n    } while (maxParentIx < parentIx--);\n\n    if (context) {\n        const data = getBEMDataFromContext(context);\n        if (data.block) {\n            return data.block;\n        }\n    }\n\n    return '';\n}\n\nfunction findBlockName(classNames: string[]): string | undefined {\n    return find(classNames, blockCandidates1)\n        || find(classNames, blockCandidates2)\n        || void 0;\n}\n\n/**\n * Finds class name from given list which may be used as block name\n */\nfunction find(classNames: string[], filter: (className: string) => boolean): string | void {\n    for (const cl of classNames) {\n        if (reElement.test(cl) || reModifier.test(cl)) {\n            break;\n        }\n\n        if (filter(cl)) {\n            return cl;\n        }\n    }\n}\n\nfunction updateClass(node: AbbreviationNode, value: string) {\n    for (const attr of node.attributes!) {\n        if (attr.name === 'class') {\n            attr.value = [value];\n            break;\n        }\n    }\n}\n\nfunction stringifyValue(value: Value[]): string {\n    let result = '';\n\n    for (const t of value) {\n        result += typeof t === 'string' ? t : t.name;\n    }\n\n    return result;\n}\n\nfunction uniqueClass<T>(item: T, ix: number, arr: T[]): boolean {\n    return !!item && arr.indexOf(item) === ix;\n}\n"
  },
  {
    "path": "src/markup/addon/label.ts",
    "content": "import type { AbbreviationAttribute, AbbreviationNode } from '@emmetio/abbreviation';\nimport { find } from '../utils';\n\n/**\n * Preprocessor of `<label>` element: if it contains `<input>`, remove `for` attribute\n * and `id` from input\n */\nexport default function label(node: AbbreviationNode) {\n    if (node.name === 'label') {\n        const input = find(node, n => (n.name === 'input' || n.name === 'textarea'));\n        if (input) {\n            // Remove empty `for` attribute\n            if (node.attributes) {\n                node.attributes = node.attributes.filter(attr => {\n                    return !(attr.name === 'for' && isEmptyAttribute(attr));\n                });\n            }\n\n            // Remove empty `id` attribute\n            if (input.attributes) {\n                input.attributes = input.attributes.filter(attr => {\n                    return !(attr.name === 'id' && isEmptyAttribute(attr));\n                });\n            }\n        }\n    }\n}\n\nfunction isEmptyAttribute(attr: AbbreviationAttribute) {\n    if (!attr.value) {\n        return true;\n    }\n\n    if (attr.value.length === 1) {\n        const token = attr.value[0];\n        if (token && typeof token !== 'string' && !token.name) {\n            // Attribute contains field\n            return true;\n        }\n    }\n\n    return false;\n}\n"
  },
  {
    "path": "src/markup/addon/xsl.ts",
    "content": "import type { AbbreviationNode, AbbreviationAttribute } from '@emmetio/abbreviation';\n\n/**\n * XSL transformer: removes `select` attributes from certain nodes that contain\n * children\n */\nexport default function xsl(node: AbbreviationNode) {\n    if (matchesName(node.name) && node.attributes && (node.children.length || node.value)) {\n        node.attributes = node.attributes.filter(isAllowed);\n    }\n}\n\nfunction isAllowed(attr: AbbreviationAttribute): boolean {\n    return attr.name !== 'select';\n}\n\nfunction matchesName(name?: string): boolean {\n    return name === 'xsl:variable' || name === 'xsl:with-param';\n}\n"
  },
  {
    "path": "src/markup/attributes.ts",
    "content": "import type { AbbreviationAttribute, AbbreviationNode, Value } from '@emmetio/abbreviation';\nimport type { Config } from '../config';\n\n/**\n * Merges attributes in current node: de-duplicates attributes with the same name\n * and merges class names\n */\nexport default function mergeAttributes(node: AbbreviationNode, config: Config) {\n    if (!node.attributes) {\n        return;\n    }\n\n    const attributes: AbbreviationAttribute[] = [];\n    const lookup: { [name: string]: AbbreviationAttribute } = {};\n\n    for (const attr of node.attributes) {\n        if (attr.name) {\n            const attrName = attr.name;\n            if (attrName in lookup) {\n                const prev = lookup[attrName];\n                if (attrName === 'class') {\n                    prev.value = mergeValue(prev.value, attr.value, ' ');\n                } else {\n                    mergeDeclarations(prev, attr, config);\n                }\n            } else {\n                // Create new attribute instance so we can safely modify it later\n                attributes.push(lookup[attrName] = { ...attr });\n            }\n        } else {\n            attributes.push(attr);\n        }\n    }\n\n    node.attributes = attributes;\n}\n\n/**\n * Merges two token lists into single list. Adjacent strings are merged together\n */\nfunction mergeValue(prev?: Value[], next?: Value[], glue?: string): Value[] | undefined {\n    if (prev && next) {\n        if (prev.length && glue) {\n            append(prev, glue);\n        }\n\n        for (const t of next) {\n            append(prev, t);\n        }\n\n        return prev;\n    }\n\n    const result = prev || next;\n    return result && result.slice();\n}\n\n/**\n * Merges data from `src` attribute into `dest` and returns it\n */\nfunction mergeDeclarations(dest: AbbreviationAttribute, src: AbbreviationAttribute, config: Config): AbbreviationAttribute {\n    dest.name = src.name;\n\n    if (!config.options['output.reverseAttributes']) {\n        dest.value = src.value;\n    }\n\n    // Keep high-priority properties\n    if (!dest.implied) {\n        dest.implied = src.implied;\n    }\n\n    if (!dest.boolean) {\n        dest.boolean = src.boolean;\n    }\n\n    if (dest.valueType !== 'expression') {\n        dest.valueType = src.valueType;\n    }\n\n    return dest;\n}\n\nfunction append(tokens: Value[], value: Value) {\n    const lastIx = tokens.length - 1;\n    if (typeof tokens[lastIx] === 'string' && typeof value === 'string') {\n        tokens[lastIx] += value;\n    } else {\n        tokens.push(value);\n    }\n}\n"
  },
  {
    "path": "src/markup/format/comment.ts",
    "content": "import type { AbbreviationNode, Value } from '@emmetio/abbreviation';\nimport { pushString } from '../../output-stream';\nimport type { WalkState } from './walk';\nimport { pushTokens } from './utils';\nimport template, { type TemplateToken } from './template';\nimport type { HTMLWalkState } from './html';\nimport type { Config } from '../../config';\n\nexport interface CommentWalkState {\n    enabled: boolean;\n    trigger: string[];\n    before?: TemplateToken[];\n    after?: TemplateToken[];\n}\n\nexport function createCommentState(config: Config): CommentWalkState {\n    const { options } = config;\n    return {\n        enabled: options['comment.enabled'],\n        trigger: options['comment.trigger'],\n        before: options['comment.before'] ? template(options['comment.before']) : void 0,\n        after: options['comment.after'] ? template(options['comment.after']) : void 0\n    };\n}\n\n/**\n * Adds comment prefix for given node, if required\n */\nexport function commentNodeBefore(node: AbbreviationNode, state: HTMLWalkState) {\n    if (shouldComment(node, state) && state.comment.before) {\n        output(node, state.comment.before, state);\n    }\n}\n\n/**\n * Adds comment suffix for given node, if required\n */\nexport function commentNodeAfter(node: AbbreviationNode, state: HTMLWalkState) {\n    if (shouldComment(node, state) && state.comment.after) {\n        output(node, state.comment.after, state);\n    }\n}\n\n/**\n * Check if given node should be commented\n */\nfunction shouldComment(node: AbbreviationNode, state: HTMLWalkState): boolean {\n    const { comment } = state;\n\n    if (!comment.enabled || !comment.trigger || !node.name || !node.attributes) {\n        return false;\n    }\n\n    for (const attr of node.attributes) {\n        if (attr.name && comment.trigger.includes(attr.name)) {\n            return true;\n        }\n    }\n\n    return false;\n}\n\n/**\n * Pushes given template tokens into output stream\n */\nfunction output(node: AbbreviationNode, tokens: TemplateToken[], state: WalkState) {\n    const attrs: { [name: string]: Value[] } = {};\n    const { out } = state;\n\n    // Collect attributes payload\n    for (const attr of node.attributes!) {\n        if (attr.name && attr.value) {\n            attrs[attr.name.toUpperCase()] = attr.value;\n        }\n    }\n\n    // Output parsed tokens\n    for (const token of tokens) {\n        if (typeof token === 'string') {\n            pushString(out, token);\n        } else if (attrs[token.name]) {\n            pushString(out, token.before);\n            pushTokens(attrs[token.name], state);\n            pushString(out, token.after);\n        }\n    }\n}\n"
  },
  {
    "path": "src/markup/format/haml.ts",
    "content": "import type { Abbreviation } from '@emmetio/abbreviation';\nimport indentFormat from './indent-format';\nimport type { Config } from '../../config';\n\nexport default function haml(abbr: Abbreviation, config: Config): string {\n    return indentFormat(abbr, config, {\n        beforeName: '%',\n        beforeAttribute: '(',\n        afterAttribute: ')',\n        glueAttribute: ' ',\n        afterTextLine: ' |',\n        booleanValue: 'true',\n        selfClose: '/'\n    });\n}\n"
  },
  {
    "path": "src/markup/format/html.ts",
    "content": "import type { Abbreviation, AbbreviationNode, AbbreviationAttribute, Value } from '@emmetio/abbreviation';\nimport { pushNewline, pushString, tagName, selfClose, attrName, isBooleanAttribute, attrQuote, isInline, expressionStart, expressionEnd } from '../../output-stream';\nimport walk, { type WalkState, createWalkState } from './walk';\nimport { caret, isInlineElement, isSnippet, isField, pushTokens, shouldOutputAttribute } from './utils';\nimport { commentNodeBefore, commentNodeAfter, type CommentWalkState, createCommentState } from './comment';\nimport type { Config } from '../../config';\n\ntype WalkNext = (node: AbbreviationNode, index: number, items: AbbreviationNode[]) => void;\n\nexport interface HTMLWalkState extends WalkState {\n    comment: CommentWalkState;\n}\n\nconst htmlTagRegex = /^<([\\w\\-:]+)[\\s>]/;\nconst reservedKeywords = new Set([\n    'for', 'while', 'of', 'async', 'await', 'const', 'let', 'var', 'continue',\n    'break', 'debugger', 'do', 'export', 'import', 'in', 'instanceof', 'new', 'return',\n    'switch', 'this', 'throw', 'try', 'catch', 'typeof', 'void', 'with', 'yield'\n]);\n\nexport default function html(abbr: Abbreviation, config: Config): string {\n    const state = createWalkState(config) as HTMLWalkState;\n    state.comment = createCommentState(config);\n    walk(abbr, element, state);\n    return state.out.value;\n}\n\n/**\n * Outputs `node` content to output stream of `state`\n * @param node Context node\n * @param index Index of `node` in `items`\n * @param items List of `node`’s siblings\n * @param state Current walk state\n */\nfunction element(node: AbbreviationNode, index: number, items: AbbreviationNode[], state: HTMLWalkState, next: WalkNext) {\n    const { out, config } = state;\n    const format = shouldFormat(node, index, items, state);\n\n    // Pick offset level for current node\n    const level = getIndent(state);\n    out.level += level;\n\n    format && pushNewline(out, true);\n\n    if (node.name) {\n        const name = tagName(node.name, config);\n        commentNodeBefore(node, state);\n        pushString(out, `<${name}`);\n\n        if (node.attributes) {\n            for (const attr of node.attributes) {\n                if (shouldOutputAttribute(attr)) {\n                    pushAttribute(attr, state);\n                }\n            }\n        }\n\n        if (node.selfClosing && !node.children.length && !node.value) {\n            pushString(out, `${selfClose(config)}>`);\n        } else {\n            pushString(out, '>');\n\n            if (!pushSnippet(node, state, next)) {\n                if (node.value) {\n                    const innerFormat = node.value.some(hasNewline) || startsWithBlockTag(node.value, config);\n                    innerFormat && pushNewline(state.out, ++out.level);\n                    pushTokens(node.value, state);\n                    innerFormat && pushNewline(state.out, --out.level);\n                }\n\n                node.children.forEach(next);\n\n                if (!node.value && !node.children.length) {\n                    const innerFormat = config.options['output.formatLeafNode']\n                        || config.options['output.formatForce'].includes(node.name);\n                    innerFormat && pushNewline(state.out, ++out.level);\n                    pushTokens(caret, state);\n                    innerFormat && pushNewline(state.out, --out.level);\n                }\n            }\n\n            pushString(out, `</${name}>`);\n            commentNodeAfter(node, state);\n        }\n    } else if (!pushSnippet(node, state, next) && node.value) {\n        // A text-only node (snippet)\n        pushTokens(node.value, state);\n        node.children.forEach(next);\n    }\n\n    if (format && index === items.length - 1 && state.parent) {\n        const offset = isSnippet(state.parent) ? 0 : 1;\n        pushNewline(out, out.level - offset);\n    }\n\n    out.level -= level;\n}\n\n/**\n * Outputs given attribute’s content into output stream\n */\nfunction pushAttribute(attr: AbbreviationAttribute, state: WalkState) {\n    const { out, config } = state;\n\n    if (attr.name) {\n        const attributes = config.options['markup.attributes'];\n        const valuePrefix = config.options['markup.valuePrefix'];\n\n        let { name, value } = attr;\n        let lQuote = attrQuote(attr, config, true);\n        let rQuote = attrQuote(attr, config);\n\n        if (attributes) {\n            name = getMultiValue(name, attributes, attr.multiple) || name;\n        }\n\n        name = attrName(name, config);\n\n        if (config.options['jsx.enabled'] && attr.multiple) {\n            lQuote = expressionStart;\n            rQuote = expressionEnd;\n        }\n\n        const prefix = valuePrefix\n            ? getMultiValue(attr.name, valuePrefix, attr.multiple)\n            : null;\n\n        if (prefix && value?.length === 1 && typeof value[0] === 'string') {\n            // Add given prefix in object notation\n            const val = value[0] as string;\n            value = [isPropKey(val) ? `${prefix}.${val}` : `${prefix}['${val}']`];\n\n            if (config.options['jsx.enabled']) {\n                lQuote = expressionStart;\n                rQuote = expressionEnd;\n            }\n        }\n\n        if (isBooleanAttribute(attr, config) && !value) {\n            // If attribute value is omitted and it’s a boolean value, check for\n            // `compactBoolean` option: if it’s disabled, set value to attribute name\n            // (XML style)\n            if (!config.options['output.compactBoolean']) {\n                value = [name];\n            }\n        } else if (!value) {\n            value = caret;\n        }\n\n        pushString(out, ' ' + name);\n        if (value) {\n            pushString(out, '=' + lQuote);\n            pushTokens(value, state);\n            pushString(out, rQuote);\n        } else if (config.options['output.selfClosingStyle'] !== 'html') {\n            pushString(out, '=' + lQuote + rQuote);\n        }\n    }\n}\n\nexport function pushSnippet(node: AbbreviationNode, state: WalkState, next: WalkNext): boolean {\n    if (node.value && node.children.length) {\n        // We have a value and child nodes. In case if value contains fields,\n        // we should output children as a content of first field\n        const fieldIx = node.value.findIndex(isField);\n        if (fieldIx !== -1) {\n            pushTokens(node.value.slice(0, fieldIx), state);\n            const line = state.out.line;\n            let pos = fieldIx + 1;\n            node.children.forEach(next);\n\n            // If there was a line change, trim leading whitespace for better result\n            if (state.out.line !== line && typeof node.value[pos] === 'string') {\n                pushString(state.out, (node.value[pos++] as string).trimLeft());\n            }\n\n            pushTokens(node.value.slice(pos), state);\n            return true;\n        }\n    }\n\n    return false;\n}\n\n/**\n * Check if given node should be formatted in its parent context\n */\nfunction shouldFormat(node: AbbreviationNode, index: number, items: AbbreviationNode[], state: WalkState): boolean {\n    const { config, parent } = state;\n\n    if (!config.options['output.format']) {\n        return false;\n    }\n\n    if (index === 0 && !parent) {\n        // Do not format very first node\n        return false;\n    }\n\n    // Do not format single child of snippet\n    if (parent && isSnippet(parent) && items.length === 1) {\n        return false;\n    }\n\n    /**\n     * Adjacent text-only/snippet nodes\n     */\n    if (isSnippet(node)) {\n        // Adjacent text-only/snippet nodes\n        const format = isSnippet(items[index - 1]) || isSnippet(items[index + 1])\n\n            // Has newlines: looks like wrapping code fragment\n            || node.value!.some(hasNewline)\n\n            // Format as wrapper: contains children which will be outputted as field content\n            || (node.value!.some(isField) && node.children.length);\n\n        if (format) {\n            return true;\n        }\n    }\n\n    if (isInline(node, config)) {\n        // Check if inline node is the next sibling of block-level node\n        if (index === 0) {\n            // First node in parent: format if it’s followed by a block-level element\n            for (let i = 0; i < items.length; i++) {\n                if (!isInline(items[i], config)) {\n                    return true;\n                }\n            }\n        } else if (!isInline(items[index - 1], config)) {\n            // Node is right after block-level element\n            return true;\n        }\n\n        if (config.options['output.inlineBreak']) {\n            // check for adjacent inline elements before and after current element\n            let adjacentInline = 1;\n            let before = index;\n            let after = index;\n\n            while (isInlineElement(items[--before], config)) {\n                adjacentInline++;\n            }\n\n            while (isInlineElement(items[++after], config)) {\n                adjacentInline++;\n            }\n\n            if (adjacentInline >= config.options['output.inlineBreak']) {\n                return true;\n            }\n        }\n\n        // Edge case: inline node contains node that should receive formatting\n        for (let i = 0, il = node.children.length; i < il; i++) {\n            if (shouldFormat(node.children[i], i, node.children, state)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    return true;\n}\n\n/**\n * Returns indentation offset for given node\n */\nfunction getIndent(state: WalkState): number {\n    const { config, parent } = state;\n\n    if (!parent || isSnippet(parent) || (parent.name && config.options['output.formatSkip'].includes(parent.name))) {\n        return 0;\n    }\n\n    return 1;\n}\n\n/**\n * Check if given node value contains newlines\n */\nfunction hasNewline(value: Value): boolean {\n    return typeof value === 'string' && /\\r|\\n/.test(value);\n}\n\n/**\n * Check if given node value starts with block-level tag\n */\nfunction startsWithBlockTag(value: Value[], config: Config): boolean {\n    if (value.length && typeof value[0] === 'string') {\n        const matches = htmlTagRegex.exec(value[0]);\n        if (matches?.length && !config.options['inlineElements'].includes(matches[1].toLowerCase())) {\n            return true;\n        }\n    }\n    return false;\n}\n\nfunction getMultiValue(key: string, data: Record<string, string>, multiple?: boolean): string | undefined {\n    return (multiple && data[`${key}*`]) || data[key];\n}\n\nfunction isPropKey(name: string): boolean {\n    return !reservedKeywords.has(name) && /^[a-zA-Z_$][\\w_$]*$/.test(name);\n}\n"
  },
  {
    "path": "src/markup/format/indent-format.ts",
    "content": "import type { AbbreviationNode, AbbreviationAttribute, Value, Abbreviation } from '@emmetio/abbreviation';\nimport { pushString, pushNewline, push, attrName, isBooleanAttribute, attrQuote } from '../../output-stream';\nimport { pushTokens, caret, splitByLines, isSnippet, shouldOutputAttribute } from './utils';\nimport walk, { type WalkState, createWalkState, type WalkNext } from './walk';\nimport type { Config } from '../../config';\n\n/**\n * @description Utility methods for working with indent-based markup languages\n * like HAML, Slim, Pug etc.\n */\n\ninterface AttributesCollection {\n    /** Primary element attributes: `id` and `class` */\n    primary: AbbreviationAttribute[];\n\n    /** Secondary element attributes: everything except `id` and `class` */\n    secondary: AbbreviationAttribute[];\n}\n\nexport interface IndentWalkState extends WalkState {\n    options: FormatOptions;\n}\n\nexport interface FormatOptions {\n    /** String to output before tag name */\n    beforeName?: string;\n\n    /** String to output after tag name */\n    afterName?: string;\n\n    /** String to output before secondary attribute set */\n    beforeAttribute?: string;\n\n    /** String to output after secondary attribute set */\n    afterAttribute?: string;\n\n    /** String to put between secondary attributes */\n    glueAttribute?: string;\n\n    /** Value for boolean attributes */\n    booleanValue?: string;\n\n    /** String to put before content line (if value is multiline) */\n    beforeTextLine?: string;\n\n    /** String to put after content line (if value is multiline) */\n    afterTextLine?: string;\n\n    /** String to put after self-closing elements like `br`. Mostly a `/` character */\n    selfClose?: string;\n}\n\nexport default function indentFormat(abbr: Abbreviation, config: Config, options?: Partial<FormatOptions>): string {\n    const state = createWalkState(config) as IndentWalkState;\n    state.options = options || {};\n    walk(abbr, element, state);\n    return state.out.value;\n}\n\n/**\n * Outputs `node` content to output stream of `state`\n * @param node Context node\n * @param index Index of `node` in `items`\n * @param items List of `node`’s siblings\n * @param state Current walk state\n */\nexport function element(node: AbbreviationNode, index: number, items: AbbreviationNode[], state: IndentWalkState, next: WalkNext) {\n    const { out, options } = state;\n    const { primary, secondary } = collectAttributes(node);\n\n    // Pick offset level for current node\n    const level = state.parent ? 1 : 0;\n    out.level += level;\n\n    // Do not indent top-level elements\n    if (shouldFormat(node, index, items, state)) {\n        pushNewline(out, true);\n    }\n\n    if (node.name && (node.name !== 'div' || !primary.length)) {\n        pushString(out, (options.beforeName || '') + node.name + (options.afterName || ''));\n    }\n\n    pushPrimaryAttributes(primary, state);\n    pushSecondaryAttributes(secondary.filter(shouldOutputAttribute), state);\n\n    if (node.selfClosing && !node.value && !node.children.length) {\n        if (state.options.selfClose) {\n            pushString(out, state.options.selfClose);\n        }\n    } else {\n        pushValue(node, state);\n        node.children.forEach(next);\n    }\n\n    out.level -= level;\n}\n\n/**\n * From given node, collects all attributes as `primary` (id, class) and\n * `secondary` (all the rest) lists. In most indent-based syntaxes, primary attribute\n * has special syntax\n */\nexport function collectAttributes(node: AbbreviationNode): AttributesCollection {\n    const primary: AbbreviationAttribute[] = [];\n    const secondary: AbbreviationAttribute[] = [];\n\n    if (node.attributes) {\n        for (const attr of node.attributes) {\n            if (isPrimaryAttribute(attr)) {\n                primary.push(attr);\n            } else {\n                secondary.push(attr);\n            }\n        }\n    }\n\n    return { primary, secondary };\n}\n\n/**\n * Outputs given attributes as primary into output stream\n */\nexport function pushPrimaryAttributes(attrs: AbbreviationAttribute[], state: WalkState) {\n    for (const attr of attrs) {\n        if (attr.value) {\n            if (attr.name === 'class') {\n                pushString(state.out, '.');\n                // All whitespace characters must be replaced with dots in class names\n                const tokens = attr.value.map(t => typeof t === 'string' ? t.replace(/\\s+/g, '.') : t);\n                pushTokens(tokens, state);\n            } else {\n                // ID attribute\n                pushString(state.out, '#');\n                pushTokens(attr.value, state);\n            }\n        }\n    }\n}\n\n/**\n * Outputs given attributes as secondary into output stream\n */\nexport function pushSecondaryAttributes(attrs: AbbreviationAttribute[], state: IndentWalkState) {\n    if (attrs.length) {\n        const { out, config, options } = state;\n\n        options.beforeAttribute && pushString(out, options.beforeAttribute);\n\n        for (let i = 0; i < attrs.length; i++) {\n            const attr = attrs[i];\n            pushString(out, attrName(attr.name || '', config));\n            if (isBooleanAttribute(attr, config) && !attr.value) {\n                if (!config.options['output.compactBoolean'] && options.booleanValue) {\n                    pushString(out, '=' + options.booleanValue);\n                }\n            } else {\n                pushString(out, '=' + attrQuote(attr, config, true));\n                pushTokens(attr.value || caret, state);\n                pushString(out, attrQuote(attr, config));\n            }\n\n            if (i !== attrs.length - 1 && options.glueAttribute) {\n                pushString(out, options.glueAttribute);\n            }\n        }\n\n        options.afterAttribute && pushString(out, options.afterAttribute);\n    }\n}\n\n/**\n * Outputs given node value into state output stream\n */\nexport function pushValue(node: AbbreviationNode, state: IndentWalkState) {\n    // We should either output value or add caret but for leaf nodes only (no children)\n    if (!node.value && node.children.length) {\n        return;\n    }\n\n    const value = node.value || caret;\n    const lines = splitByLines(value);\n    const { out, options } = state;\n\n    if (lines.length === 1) {\n        if (node.name || node.attributes) {\n            push(out, ' ');\n        }\n        pushTokens(value, state);\n    } else {\n        // We should format multi-line value with terminating `|` character\n        // and same line length\n        const lineLengths: number[] = [];\n        let maxLength = 0;\n\n        // Calculate lengths of all lines and max line length\n        for (const line of lines) {\n            const len = valueLength(line);\n            lineLengths.push(len);\n            if (len > maxLength) {\n                maxLength = len;\n            }\n        }\n\n        // Output each line, padded to max length\n        out.level++;\n        for (let i = 0; i < lines.length; i++) {\n            pushNewline(out, true);\n            options.beforeTextLine && push(out, options.beforeTextLine);\n            pushTokens(lines[i], state);\n            if (options.afterTextLine) {\n                push(out, ' '.repeat(maxLength - lineLengths[i]));\n                push(out, options.afterTextLine);\n            }\n        }\n        out.level--;\n    }\n}\n\nfunction isPrimaryAttribute(attr: AbbreviationAttribute): boolean {\n    return attr.name === 'class' || attr.name === 'id';\n}\n\n/**\n * Calculates string length from given tokens\n */\nfunction valueLength(tokens: Value[]): number {\n    let len = 0;\n\n    for (const token of tokens) {\n        len += typeof token === 'string' ? token.length : token.name.length;\n    }\n\n    return len;\n}\n\nfunction shouldFormat(node: AbbreviationNode, index: number, items: AbbreviationNode[], state: WalkState): boolean {\n    // Do not format first top-level element or snippets\n    if (!state.parent && index === 0) {\n        return false;\n    }\n    return !isSnippet(node);\n}\n"
  },
  {
    "path": "src/markup/format/pug.ts",
    "content": "import type { Abbreviation } from '@emmetio/abbreviation';\nimport indentFormat from './indent-format';\nimport type { Config } from '../../config';\n\nexport default function pug(abbr: Abbreviation, config: Config): string {\n    return indentFormat(abbr, config, {\n        beforeAttribute: '(',\n        afterAttribute: ')',\n        glueAttribute: ', ',\n        beforeTextLine: '| ',\n        selfClose: config.options['output.selfClosingStyle'] === 'xml' ? '/' : ''\n    });\n}\n"
  },
  {
    "path": "src/markup/format/slim.ts",
    "content": "import type { Abbreviation } from '@emmetio/abbreviation';\nimport indentFormat from './indent-format';\nimport type { Config } from '../../config';\n\nexport default function slim(abbr: Abbreviation, config: Config): string {\n    return indentFormat(abbr, config, {\n        beforeAttribute: ' ',\n        glueAttribute: ' ',\n        beforeTextLine: '| ',\n        selfClose: '/'\n    });\n}\n"
  },
  {
    "path": "src/markup/format/template.ts",
    "content": "export type TemplateToken = string | TemplatePlaceholder;\nexport interface TemplatePlaceholder {\n    before: string;\n    after: string;\n    name: string;\n}\n\ninterface TokenScanner {\n    text: string;\n    pos: number;\n}\n\nconst enum TemplateChars {\n    /** `[` character */\n    Start = 91,\n\n    /** `]` character */\n    End = 93,\n\n    /* `_` character */\n    Underscore = 95,\n\n    /* `-` character */\n    Dash = 45,\n}\n\n/**\n * Splits given string into template tokens.\n * Template is a string which contains placeholders which are uppercase names\n * between `[` and `]`, for example: `[PLACEHOLDER]`.\n * Unlike other templates, a placeholder may contain extra characters before and\n * after name: `[%PLACEHOLDER.]`. If data for `PLACEHOLDER` is defined, it will\n * be outputted with with these extra character, otherwise will be completely omitted.\n */\nexport default function template(text: string): TemplateToken[] {\n    const tokens: TemplateToken[] = [];\n    const scanner: TokenScanner = { pos: 0, text };\n    let placeholder: TemplatePlaceholder | undefined;\n    let offset = scanner.pos;\n    let pos = scanner.pos;\n\n    while (scanner.pos < scanner.text.length) {\n        pos = scanner.pos;\n        if (placeholder = consumePlaceholder(scanner)) {\n            if (offset !== scanner.pos) {\n                tokens.push(text.slice(offset, pos));\n            }\n            tokens.push(placeholder);\n            offset = scanner.pos;\n        } else {\n            scanner.pos++;\n        }\n    }\n\n    if (offset !== scanner.pos) {\n        tokens.push(text.slice(offset));\n    }\n\n    return tokens;\n}\n\n/**\n * Consumes placeholder like `[#ID]` from given scanner\n */\nfunction consumePlaceholder(scanner: TokenScanner): TemplatePlaceholder | undefined {\n    if (peek(scanner) === TemplateChars.Start) {\n        const start = ++scanner.pos;\n        let namePos = start;\n        let afterPos = start;\n        let stack = 1;\n\n        while (scanner.pos < scanner.text.length) {\n            const code = peek(scanner);\n            if (isTokenStart(code)) {\n                namePos = scanner.pos;\n                while (isToken(peek(scanner))) {\n                    scanner.pos++;\n                }\n                afterPos = scanner.pos;\n            } else {\n                if (code === TemplateChars.Start) {\n                    stack++;\n                } else if (code === TemplateChars.End) {\n                    if (--stack === 0) {\n                        return {\n                            before: scanner.text.slice(start, namePos),\n                            after: scanner.text.slice(afterPos, scanner.pos++),\n                            name: scanner.text.slice(namePos, afterPos)\n                        };\n                    }\n                }\n\n                scanner.pos++;\n            }\n        }\n    }\n}\n\nfunction peek(scanner: TokenScanner, pos = scanner.pos): number {\n    return scanner.text.charCodeAt(pos);\n}\n\nfunction isTokenStart(code: number): boolean {\n    return code >= 65 && code <= 90; // A-Z\n}\n\nfunction isToken(code: number): boolean {\n    return isTokenStart(code)\n        || (code > 47 && code < 58) /* 0-9 */\n        || code === TemplateChars.Underscore\n        || code === TemplateChars.Dash;\n}\n"
  },
  {
    "path": "src/markup/format/utils.ts",
    "content": "import type { AbbreviationNode, Field, Value, AbbreviationAttribute } from '@emmetio/abbreviation';\nimport type { WalkState } from './walk';\nimport { pushString, pushField, isInline } from '../../output-stream';\nimport type { Config } from '../../config';\n\nexport const caret = [{ type: 'Field', index: 0, name: '' } as Field];\n\n/**\n * Check if given node is a snippet: a node without name and attributes\n */\nexport function isSnippet(node?: AbbreviationNode): boolean {\n    return node ? !node.name && !node.attributes : false;\n}\n\n/**\n * Check if given node is inline-level element, e.g. element with explicitly\n * defined node name\n */\nexport function isInlineElement(node: AbbreviationNode | undefined, config: Config): boolean {\n    return node ? isInline(node, config) : false;\n}\n\n/**\n * Check if given value token is a field\n */\nexport function isField(token: Value): token is Field {\n    return typeof token === 'object' && token.type === 'Field';\n}\n\nexport function pushTokens(tokens: Value[], state: WalkState) {\n    const { out } = state;\n    let largestIndex = -1;\n\n    for (const t of tokens) {\n        if (typeof t === 'string') {\n            pushString(out, t);\n        } else {\n            pushField(out, state.field + t.index!, t.name);\n            if (t.index! > largestIndex) {\n                largestIndex = t.index!;\n            }\n        }\n    }\n\n    if (largestIndex !== -1) {\n        state.field += largestIndex + 1;\n    }\n}\n\n/**\n * Splits given value token by lines: returns array where each entry is a token list\n * for a single line\n */\nexport function splitByLines(tokens: Value[]): Value[][] {\n    const result: Value[][] = [];\n    let line: Value[] = [];\n\n    for (const t of tokens) {\n        if (typeof t === 'string') {\n            const lines = t.split(/\\r\\n?|\\n/g);\n            line.push(lines.shift() || '');\n            while (lines.length) {\n                result.push(line);\n                line = [lines.shift() || ''];\n            }\n        } else {\n            line.push(t);\n        }\n    }\n\n    line.length && result.push(line);\n    return result;\n}\n\n/**\n * Check if given attribute should be outputted\n */\nexport function shouldOutputAttribute(attr: AbbreviationAttribute): boolean {\n    // In case if attribute is implied, check if it has a defined value:\n    // either non-empty value or quoted empty value\n    return !attr.implied || attr.valueType !== 'raw' || (!!attr.value && attr.value.length > 0);\n}\n"
  },
  {
    "path": "src/markup/format/walk.ts",
    "content": "import type { AbbreviationNode, Abbreviation } from '@emmetio/abbreviation';\nimport createOutputStream, { type OutputStream } from '../../output-stream';\nimport type { Config } from '../../config';\n\nexport type WalkNext = (node: AbbreviationNode, index: number, items: AbbreviationNode[]) => void;\nexport type Visitor<S extends WalkState> = (node: AbbreviationNode, index: number, items: AbbreviationNode[], state: S, next: WalkNext) => void;\n\nexport interface WalkState {\n    /** Context node */\n    current: AbbreviationNode;\n\n    /** Immediate parent of currently iterated method */\n    parent?: AbbreviationNode;\n\n    /** List of all ancestors of context node */\n    ancestors: AbbreviationNode[];\n\n    /** Current output config */\n    config: Config;\n\n    /** Output stream */\n    out: OutputStream;\n\n    /** Current field index, used to output field marks for editor tabstops */\n    field: number;\n}\n\nexport default function walk<S extends WalkState>(abbr: Abbreviation, visitor: Visitor<S>, state: S) {\n    const callback = (ctx: AbbreviationNode, index: number, items: AbbreviationNode[]) => {\n        const { parent, current } = state;\n        state.parent = current;\n        state.current = ctx;\n        visitor(ctx, index, items, state, next);\n        state.current = current;\n        state.parent = parent;\n    };\n\n    const next: WalkNext = (node, index, items) => {\n        state.ancestors.push(state.current);\n        callback(node, index, items);\n        state.ancestors.pop();\n    };\n\n    abbr.children.forEach(callback);\n}\n\nexport function createWalkState(config: Config): WalkState {\n    return {\n        // @ts-ignore: Will set value in iterator\n        current: null,\n        parent: void 0,\n        ancestors: [],\n        config,\n        field: 1,\n        out: createOutputStream(config.options)\n    };\n}\n"
  },
  {
    "path": "src/markup/implicit-tag.ts",
    "content": "import type { AbbreviationNode } from '@emmetio/abbreviation';\nimport { isNode, type Container } from './utils';\nimport type { Config } from '../config';\nimport { isInline } from '../output-stream';\n\nconst elementMap: { [name: string]: string } = {\n    p: 'span',\n    ul: 'li',\n    ol: 'li',\n    table: 'tr',\n    tr: 'td',\n    tbody: 'tr',\n    thead: 'tr',\n    tfoot: 'tr',\n    colgroup: 'col',\n    select: 'option',\n    optgroup: 'option',\n    audio: 'source',\n    video: 'source',\n    object: 'param',\n    map: 'area'\n};\n\nexport default function implicitTag(node: AbbreviationNode, ancestors: Container[], config: Config) {\n    if (!node.name && node.attributes) {\n        resolveImplicitTag(node, ancestors, config);\n    }\n}\n\nexport function resolveImplicitTag(node: AbbreviationNode, ancestors: Container[], config: Config) {\n    const parent = getParentElement(ancestors);\n    const contextName = config.context ? config.context.name : '';\n    const parentName = lowercase(parent ? parent.name : contextName);\n    node.name = elementMap[parentName]\n        || (isInline(parentName, config) ? 'span' : 'div');\n}\n\nfunction lowercase(str?: string): string {\n    return (str || '').toLowerCase();\n}\n\n/**\n * Returns closest element node from given ancestors list\n */\nfunction getParentElement(ancestors: Container[]): AbbreviationNode | undefined {\n    for (let i = ancestors.length - 1; i >= 0; i--) {\n        const elem = ancestors[i];\n        if (isNode(elem)) {\n            return elem;\n        }\n    }\n}\n"
  },
  {
    "path": "src/markup/index.ts",
    "content": "import abbreviation from '@emmetio/abbreviation';\nimport type { Abbreviation, AbbreviationNode, ParserOptions } from '@emmetio/abbreviation';\nimport attributes from './attributes';\nimport snippets from './snippets';\nimport implicitTag from './implicit-tag';\nimport lorem from './lorem';\nimport xsl from './addon/xsl';\nimport bem from './addon/bem';\nimport label from './addon/label';\nimport html from './format/html';\nimport haml from './format/haml';\nimport slim from './format/slim';\nimport pug from './format/pug';\nimport type { Config } from '../config';\nimport { walk, type Container } from './utils';\n\ntype Formatter = (abbr: Abbreviation, config: Config) => string;\n\nconst formatters: { [syntax: string]: Formatter } = { html, haml, slim, pug };\n\n/**\n * Parses given Emmet abbreviation into a final abbreviation tree with all\n * required transformations applied\n */\nexport default function parse(abbr: string | Abbreviation, config: Config): Abbreviation {\n    let oldTextValue: string | string[] | undefined;\n    if (typeof abbr === 'string') {\n        const parseOpt: ParserOptions = { ...config };\n\n        if (config.options['jsx.enabled']) {\n            parseOpt.jsx = true;\n        }\n\n        if (config.options['markup.href']) {\n            parseOpt.href = true;\n        }\n\n        abbr = abbreviation(abbr, parseOpt);\n\n        // remove text field before snippets(abbr, config) call\n        // as abbreviation(abbr, parseOpt) already handled it\n        oldTextValue = config.text;\n        config.text = undefined;\n    }\n\n    // Run abbreviation resolve in two passes:\n    // 1. Map each node to snippets, which are abbreviations as well. A single snippet\n    // may produce multiple nodes\n    // 2. Transform every resolved node\n    abbr = snippets(abbr, config);\n    walk(abbr, transform, config);\n    config.text = oldTextValue ?? config.text;\n    return abbr;\n}\n\n/**\n * Converts given abbreviation to string according to provided `config`\n */\nexport function stringify(abbr: Abbreviation, config: Config): string {\n    const formatter: Formatter = formatters[config.syntax] || html;\n    return formatter(abbr, config);\n}\n\n/**\n * Modifies given node and prepares it for output\n */\nfunction transform(node: AbbreviationNode, ancestors: Container[], config: Config) {\n    implicitTag(node, ancestors, config);\n    attributes(node, config);\n    lorem(node, ancestors, config);\n\n    if (config.syntax === 'xsl') {\n        xsl(node);\n    }\n\n    if (config.type === 'markup') {\n        label(node);\n    }\n\n    if (config.options['bem.enabled']) {\n        bem(node, ancestors, config);\n    }\n}\n"
  },
  {
    "path": "src/markup/lorem/index.ts",
    "content": "import type { AbbreviationNode, Repeater } from '@emmetio/abbreviation';\nimport type { Container } from '../utils';\nimport type { Config } from '../../config';\nimport { resolveImplicitTag } from '../implicit-tag';\nimport latin from './latin.json' assert { type: 'json' };\nimport ru from './russian.json' assert { type: 'json' };\nimport sp from './spanish.json' assert { type: 'json' };\n\ninterface LoremVocabulary {\n    common: string[];\n    words: string[];\n}\n\nconst vocabularies: { [lang: string]: LoremVocabulary } = { ru, sp, latin };\nconst reLorem = /^lorem([a-z]*)(\\d*)(-\\d*)?$/i;\n\nexport default function lorem(node: AbbreviationNode, ancestors: Container[], config: Config) {\n    let m: RegExpMatchArray | null;\n    if (node.name && (m = node.name.match(reLorem))) {\n        const db: LoremVocabulary = vocabularies[m[1]] || vocabularies.latin;\n        const minWordCount = m[2] ? Math.max(1, Number(m[2])) : 30;\n        const maxWordCount = m[3] ? Math.max(minWordCount, Number(m[3].slice(1))) : minWordCount;\n        const wordCount = rand(minWordCount, maxWordCount);\n        const repeat = node.repeat || findRepeater(ancestors);\n\n        node.name = node.attributes = void 0;\n        node.value = [paragraph(db, wordCount, !repeat || repeat.value === 0)];\n\n        if (node.repeat && ancestors.length > 1) {\n            resolveImplicitTag(node, ancestors, config);\n        }\n    }\n}\n\n/**\n * Returns random integer between <code>from</code> and <code>to</code> values\n */\nfunction rand(from: number, to: number): number {\n    return Math.floor(Math.random() * (to - from) + from);\n}\n\nfunction sample(arr: string[], count: number): string[] {\n    const len = arr.length;\n    const iterations = Math.min(len, count);\n    const result: string[] = [];\n\n    while (result.length < iterations) {\n        const str = arr[rand(0, len)];\n        if (!result.includes(str)) {\n            result.push(str);\n        }\n    }\n\n    return result;\n}\n\nfunction choice(val: string): string {\n    return val[rand(0, val.length - 1)];\n}\n\nfunction sentence(words: string[], end?: string): string {\n    if (words.length) {\n        words = [capitalize(words[0])].concat(words.slice(1));\n    }\n\n    return words.join(' ') + (end || choice('?!...')); // more dots than question marks\n}\n\nfunction capitalize(word: string): string {\n    return word[0].toUpperCase() + word.slice(1);\n}\n\n/**\n * Insert commas at randomly selected words. This function modifies values\n * inside `words` array\n */\nfunction insertCommas(words: string[]): string[] {\n    if (words.length < 2) {\n        return words;\n    }\n\n    words = words.slice();\n    const len = words.length;\n    const hasComma = /,$/;\n    let totalCommas = 0;\n\n    if (len > 3 && len <= 6) {\n        totalCommas = rand(0, 1);\n    } else if (len > 6 && len <= 12) {\n        totalCommas = rand(0, 2);\n    } else {\n        totalCommas = rand(1, 4);\n    }\n\n    for (let i = 0, pos: number; i < totalCommas; i++) {\n        pos = rand(0, len - 2);\n        if (!hasComma.test(words[pos])) {\n            words[pos] += ',';\n        }\n    }\n\n    return words;\n}\n\n/**\n * Generate a paragraph of \"Lorem ipsum\" text\n * @param dict Words dictionary\n * @param wordCount Words count in paragraph\n * @param startWithCommon Should paragraph start with common \"lorem ipsum\" sentence.\n */\nfunction paragraph(dict: LoremVocabulary, wordCount: number, startWithCommon: boolean): string {\n    const result: string[] = [];\n    let totalWords = 0;\n    let words: string[];\n\n    if (startWithCommon && dict.common) {\n        words = dict.common.slice(0, wordCount);\n        totalWords += words.length;\n        result.push(sentence(insertCommas(words), '.'));\n    }\n\n    while (totalWords < wordCount) {\n        words = sample(dict.words, Math.min(rand(2, 30), wordCount - totalWords));\n        totalWords += words.length;\n        result.push(sentence(insertCommas(words)));\n    }\n\n    return result.join(' ');\n}\n\nfunction findRepeater(ancestors: Container[]): Repeater | void {\n    for (let i = ancestors.length - 1; i >= 0; i--) {\n        const element = ancestors[i];\n        if (element.type === 'AbbreviationNode' && element.repeat) {\n            return element.repeat;\n        }\n    }\n}\n"
  },
  {
    "path": "src/markup/lorem/latin.json",
    "content": "{\n\t\"common\": [\"lorem\", \"ipsum\", \"dolor\", \"sit\", \"amet\", \"consectetur\", \"adipisicing\", \"elit\"],\n\t\"words\": [\"exercitationem\", \"perferendis\", \"perspiciatis\", \"laborum\", \"eveniet\",\n\t\t\"sunt\", \"iure\", \"nam\", \"nobis\", \"eum\", \"cum\", \"officiis\", \"excepturi\",\n\t\t\"odio\", \"consectetur\", \"quasi\", \"aut\", \"quisquam\", \"vel\", \"eligendi\",\n\t\t\"itaque\", \"non\", \"odit\", \"tempore\", \"quaerat\", \"dignissimos\",\n\t\t\"facilis\", \"neque\", \"nihil\", \"expedita\", \"vitae\", \"vero\", \"ipsum\",\n\t\t\"nisi\", \"animi\", \"cumque\", \"pariatur\", \"velit\", \"modi\", \"natus\",\n\t\t\"iusto\", \"eaque\", \"sequi\", \"illo\", \"sed\", \"ex\", \"et\", \"voluptatibus\",\n\t\t\"tempora\", \"veritatis\", \"ratione\", \"assumenda\", \"incidunt\", \"nostrum\",\n\t\t\"placeat\", \"aliquid\", \"fuga\", \"provident\", \"praesentium\", \"rem\",\n\t\t\"necessitatibus\", \"suscipit\", \"adipisci\", \"quidem\", \"possimus\",\n\t\t\"voluptas\", \"debitis\", \"sint\", \"accusantium\", \"unde\", \"sapiente\",\n\t\t\"voluptate\", \"qui\", \"aspernatur\", \"laudantium\", \"soluta\", \"amet\",\n\t\t\"quo\", \"aliquam\", \"saepe\", \"culpa\", \"libero\", \"ipsa\", \"dicta\",\n\t\t\"reiciendis\", \"nesciunt\", \"doloribus\", \"autem\", \"impedit\", \"minima\",\n\t\t\"maiores\", \"repudiandae\", \"ipsam\", \"obcaecati\", \"ullam\", \"enim\",\n\t\t\"totam\", \"delectus\", \"ducimus\", \"quis\", \"voluptates\", \"dolores\",\n\t\t\"molestiae\", \"harum\", \"dolorem\", \"quia\", \"voluptatem\", \"molestias\",\n\t\t\"magni\", \"distinctio\", \"omnis\", \"illum\", \"dolorum\", \"voluptatum\", \"ea\",\n\t\t\"quas\", \"quam\", \"corporis\", \"quae\", \"blanditiis\", \"atque\", \"deserunt\",\n\t\t\"laboriosam\", \"earum\", \"consequuntur\", \"hic\", \"cupiditate\",\n\t\t\"quibusdam\", \"accusamus\", \"ut\", \"rerum\", \"error\", \"minus\", \"eius\",\n\t\t\"ab\", \"ad\", \"nemo\", \"fugit\", \"officia\", \"at\", \"in\", \"id\", \"quos\",\n\t\t\"reprehenderit\", \"numquam\", \"iste\", \"fugiat\", \"sit\", \"inventore\",\n\t\t\"beatae\", \"repellendus\", \"magnam\", \"recusandae\", \"quod\", \"explicabo\",\n\t\t\"doloremque\", \"aperiam\", \"consequatur\", \"asperiores\", \"commodi\",\n\t\t\"optio\", \"dolor\", \"labore\", \"temporibus\", \"repellat\", \"veniam\",\n\t\t\"architecto\", \"est\", \"esse\", \"mollitia\", \"nulla\", \"a\", \"similique\",\n\t\t\"eos\", \"alias\", \"dolore\", \"tenetur\", \"deleniti\", \"porro\", \"facere\",\n\t\t\"maxime\", \"corrupti\"]\n}\n"
  },
  {
    "path": "src/markup/lorem/russian.json",
    "content": "{\n\t\"common\": [\"далеко-далеко\", \"за\", \"словесными\", \"горами\", \"в стране\", \"гласных\", \"и согласных\", \"живут\", \"рыбные\", \"тексты\"],\n\t\"words\": [\"вдали\", \"от всех\", \"они\", \"буквенных\", \"домах\", \"на берегу\", \"семантика\",\n\t\t\"большого\", \"языкового\", \"океана\", \"маленький\", \"ручеек\", \"даль\",\n\t\t\"журчит\", \"по всей\", \"обеспечивает\", \"ее\",\"всеми\", \"необходимыми\",\n\t\t\"правилами\", \"эта\", \"парадигматическая\", \"страна\", \"которой\", \"жаренные\",\n\t\t\"предложения\", \"залетают\", \"прямо\", \"рот\", \"даже\", \"всемогущая\",\n\t\t\"пунктуация\", \"не\", \"имеет\", \"власти\", \"над\", \"рыбными\", \"текстами\",\n\t\t\"ведущими\", \"безорфографичный\", \"образ\", \"жизни\", \"однажды\", \"одна\",\n\t\t\"маленькая\", \"строчка\",\"рыбного\", \"текста\", \"имени\", \"lorem\", \"ipsum\",\n\t\t\"решила\", \"выйти\", \"большой\", \"мир\", \"грамматики\", \"великий\", \"оксмокс\",\n\t\t\"предупреждал\", \"о\", \"злых\", \"запятых\", \"диких\", \"знаках\", \"вопроса\",\n\t\t\"коварных\", \"точках\", \"запятой\", \"но\", \"текст\", \"дал\", \"сбить\",\n\t\t\"себя\", \"толку\", \"он\", \"собрал\", \"семь\", \"своих\", \"заглавных\", \"букв\",\n\t\t\"подпоясал\", \"инициал\", \"за\", \"пояс\", \"пустился\", \"дорогу\",\n\t\t\"взобравшись\", \"первую\", \"вершину\", \"курсивных\", \"гор\", \"бросил\",\n\t\t\"последний\", \"взгляд\", \"назад\", \"силуэт\", \"своего\", \"родного\", \"города\",\n\t\t\"буквоград\", \"заголовок\", \"деревни\", \"алфавит\", \"подзаголовок\", \"своего\",\n\t\t\"переулка\", \"грустный\", \"реторический\", \"вопрос\", \"скатился\", \"его\",\n\t\t\"щеке\", \"продолжил\", \"свой\", \"путь\", \"дороге\", \"встретил\", \"рукопись\",\n\t\t\"она\", \"предупредила\",  \"моей\", \"все\", \"переписывается\", \"несколько\",\n\t\t\"раз\", \"единственное\", \"что\", \"меня\", \"осталось\", \"это\", \"приставка\",\n\t\t\"возвращайся\", \"ты\", \"лучше\", \"свою\", \"безопасную\", \"страну\", \"послушавшись\",\n\t\t\"рукописи\", \"наш\", \"продолжил\", \"свой\", \"путь\", \"вскоре\", \"ему\",\n\t\t\"повстречался\", \"коварный\", \"составитель\", \"рекламных\", \"текстов\",\n\t\t\"напоивший\", \"языком\", \"речью\", \"заманивший\", \"свое\", \"агентство\",\n\t\t\"которое\", \"использовало\", \"снова\", \"снова\", \"своих\", \"проектах\",\n\t\t\"если\", \"переписали\", \"то\", \"живет\", \"там\", \"до\", \"сих\", \"пор\"]\n}\n"
  },
  {
    "path": "src/markup/lorem/spanish.json",
    "content": "{\n\t\"common\": [\"mujer\", \"uno\", \"dolor\", \"más\", \"de\", \"poder\", \"mismo\", \"si\"],\n\t\"words\": [\"ejercicio\", \"preferencia\", \"perspicacia\", \"laboral\", \"paño\",\n\t\t\"suntuoso\", \"molde\", \"namibia\", \"planeador\", \"mirar\", \"demás\", \"oficinista\", \"excepción\",\n\t\t\"odio\", \"consecuencia\", \"casi\", \"auto\", \"chicharra\", \"velo\", \"elixir\",\n\t\t\"ataque\", \"no\", \"odio\", \"temporal\", \"cuórum\", \"dignísimo\",\n\t\t\"facilismo\", \"letra\", \"nihilista\", \"expedición\", \"alma\", \"alveolar\", \"aparte\",\n\t\t\"león\", \"animal\", \"como\", \"paria\", \"belleza\", \"modo\", \"natividad\",\n\t\t\"justo\", \"ataque\", \"séquito\", \"pillo\", \"sed\", \"ex\", \"y\", \"voluminoso\",\n\t\t\"temporalidad\", \"verdades\", \"racional\", \"asunción\", \"incidente\", \"marejada\",\n\t\t\"placenta\", \"amanecer\", \"fuga\", \"previsor\", \"presentación\", \"lejos\",\n\t\t\"necesariamente\", \"sospechoso\", \"adiposidad\", \"quindío\", \"pócima\",\n\t\t\"voluble\", \"débito\", \"sintió\", \"accesorio\", \"falda\", \"sapiencia\",\n\t\t\"volutas\", \"queso\", \"permacultura\", \"laudo\", \"soluciones\", \"entero\",\n\t\t\"pan\", \"litro\", \"tonelada\", \"culpa\", \"libertario\", \"mosca\", \"dictado\",\n\t\t\"reincidente\", \"nascimiento\", \"dolor\", \"escolar\", \"impedimento\", \"mínima\",\n\t\t\"mayores\", \"repugnante\", \"dulce\", \"obcecado\", \"montaña\", \"enigma\",\n\t\t\"total\", \"deletéreo\", \"décima\", \"cábala\", \"fotografía\", \"dolores\",\n\t\t\"molesto\", \"olvido\", \"paciencia\", \"resiliencia\", \"voluntad\", \"molestias\",\n\t\t\"magnífico\", \"distinción\", \"ovni\", \"marejada\", \"cerro\", \"torre\", \"y\",\n\t\t\"abogada\", \"manantial\", \"corporal\", \"agua\", \"crepúsculo\", \"ataque\", \"desierto\",\n\t\t\"laboriosamente\", \"angustia\", \"afortunado\", \"alma\", \"encefalograma\",\n\t\t\"materialidad\", \"cosas\", \"o\", \"renuncia\", \"error\", \"menos\", \"conejo\",\n\t\t\"abadía\", \"analfabeto\", \"remo\", \"fugacidad\", \"oficio\", \"en\", \"almácigo\", \"vos\", \"pan\",\n\t\t\"represión\", \"números\", \"triste\", \"refugiado\", \"trote\", \"inventor\",\n\t\t\"corchea\", \"repelente\", \"magma\", \"recusado\", \"patrón\", \"explícito\",\n\t\t\"paloma\", \"síndrome\", \"inmune\", \"autoinmune\", \"comodidad\",\n\t\t\"ley\", \"vietnamita\", \"demonio\", \"tasmania\", \"repeler\", \"apéndice\",\n\t\t\"arquitecto\", \"columna\", \"yugo\", \"computador\", \"mula\", \"a\", \"propósito\",\n\t\t\"fantasía\", \"alias\", \"rayo\", \"tenedor\", \"deleznable\", \"ventana\", \"cara\",\n\t\t\"anemia\", \"corrupto\"]\n}\n"
  },
  {
    "path": "src/markup/snippets.ts",
    "content": "import parse from '@emmetio/abbreviation';\nimport type { AbbreviationNode, AbbreviationAttribute, Abbreviation } from '@emmetio/abbreviation';\nimport { findDeepest, isNode, type Container } from './utils';\nimport type { Config } from '../config';\n\n/**\n * Finds matching snippet from `registry` and resolves it into a parsed abbreviation.\n * Resolved node is then updated or replaced with matched abbreviation tree.\n *\n * A HTML registry basically contains aliases to another Emmet abbreviations,\n * e.g. a predefined set of name, attributes and so on, possibly a complex\n * abbreviation with multiple elements. So we have to get snippet, parse it\n * and recursively resolve it.\n */\nexport default function resolveSnippets(abbr: Abbreviation, config: Config): Abbreviation {\n    const stack: string[] = [];\n    const reversed = config.options['output.reverseAttributes'];\n    const { warn } = config;\n\n    const resolve = (child: AbbreviationNode): Abbreviation | null => {\n        const snippet = child.name && config.snippets[child.name];\n        // A snippet in stack means circular reference.\n        // It can be either a user error or a perfectly valid snippet like\n        // \"img\": \"img[src alt]/\", e.g. an element with predefined shape.\n        // In any case, simply stop parsing and keep element as is\n        if (!snippet || stack.includes(snippet)) {\n            return null;\n        }\n\n        let snippetAbbr: Abbreviation;\n        try {\n            // User may add invlid snippet. In this case, silently bail out\n            snippetAbbr = parse(snippet, config);\n        } catch (err) {\n            warn?.(`Unable to parse \"${snippet}\" snippet`, err);\n            return null;\n        }\n\n        stack.push(snippet);\n        walkResolve(snippetAbbr, resolve, config);\n        stack.pop();\n\n        // Add attributes from current node into every top-level node of parsed abbreviation\n        for (const topNode of snippetAbbr.children) {\n            if (child.attributes) {\n                const from: AbbreviationAttribute[] = topNode.attributes || [];\n                const to: AbbreviationAttribute[] = child.attributes || [];\n                topNode.attributes = reversed ? to.concat(from) : from.concat(to);\n            }\n            mergeNodes(child, topNode);\n        }\n\n        return snippetAbbr;\n    };\n\n    walkResolve(abbr, resolve, config);\n    return abbr;\n}\n\nfunction walkResolve(node: Container, resolve: (node: AbbreviationNode) => Abbreviation | null, config: Config): AbbreviationNode[] {\n    let children: AbbreviationNode[] = [];\n\n    for (const child of node.children) {\n        const resolved = resolve(child);\n        if (resolved) {\n            children = children.concat(resolved.children);\n\n            const deepest = findDeepest(resolved);\n            if (isNode(deepest.node)) {\n                deepest.node.children = deepest.node.children.concat(walkResolve(child, resolve, config));\n            }\n        } else {\n            children.push(child);\n            child.children = walkResolve(child, resolve, config);\n        }\n    }\n\n    return node.children = children;\n}\n\n/**\n * Adds data from first node into second node\n */\nfunction mergeNodes(from: AbbreviationNode, to: AbbreviationNode) {\n    if (from.selfClosing) {\n        to.selfClosing = true;\n    }\n\n    if (from.value != null) {\n        to.value = from.value;\n    }\n\n    if (from.repeat) {\n        to.repeat = from.repeat;\n    }\n}\n"
  },
  {
    "path": "src/markup/utils.ts",
    "content": "import type { Abbreviation, AbbreviationNode } from '@emmetio/abbreviation';\n\nexport type Container = Abbreviation | AbbreviationNode;\nexport type WalkVisitor<S> = (node: AbbreviationNode, ancestors: Container[], state?: S) => void;\n\n/**\n * Walks over each child node of given markup abbreviation AST node (not including\n * given one) and invokes `fn` on each node.\n * The `fn` callback accepts context node, list of ancestor nodes and optional\n * state object\n */\nexport function walk<S>(node: Container, fn: WalkVisitor<S>, state?: S) {\n    const ancestors: Container[] = [node];\n    const callback = (ctx: AbbreviationNode) => {\n        fn(ctx, ancestors, state);\n        ancestors.push(ctx);\n        ctx.children.forEach(callback);\n        ancestors.pop();\n    };\n\n    node.children.forEach(callback);\n}\n\n/**\n * Finds first child node that matches given `callback`\n */\nexport function find(node: Container, callback: (node: AbbreviationNode) => boolean | undefined): AbbreviationNode | undefined {\n    for (let i = 0; i < node.children.length; i++) {\n        const child = node.children[i];\n        if (callback(child)) {\n            return child;\n        }\n\n        const result = find(child, callback);\n        if (result) {\n            return result;\n        }\n    }\n}\n\n/**\n * Finds node which is the deepest for in current node or node itself.\n */\nexport function findDeepest(node: Container): { node: Container, parent?: Container } {\n    let parent: Container | undefined;\n    while (node.children.length) {\n        parent = node;\n        node = node.children[node.children.length - 1];\n    }\n\n    return { parent, node };\n}\n\nexport function isNode(node: Container): node is AbbreviationNode {\n    return node.type === 'AbbreviationNode';\n}\n"
  },
  {
    "path": "src/output-stream.ts",
    "content": "import type { AbbreviationAttribute, AbbreviationNode } from '@emmetio/abbreviation';\nimport type { Config, Options, StringCase } from './config';\n\nexport interface OutputStream {\n    options: Options;\n    value: string;\n    level: number;\n    offset: number;\n    line: number;\n    column: number;\n}\n\nexport const expressionStart = '{';\nexport const expressionEnd = '}';\n\nexport default function createOutputStream(options: Options, level = 0): OutputStream {\n    return {\n        options,\n        value: '',\n        level,\n        offset: 0,\n        line: 0,\n        column: 0\n    };\n}\n\n/**\n * Pushes plain string into output stream without newline processing\n */\nexport function push(stream: OutputStream, text: string) {\n    const processText = stream.options['output.text'];\n    _push(stream, processText(text, stream.offset, stream.line, stream.column));\n}\n\n/**\n * Pushes given string with possible newline formatting into output\n */\nexport function pushString(stream: OutputStream, value: string) {\n    // If given value contains newlines, we should push content line-by-line and\n    // use `pushNewline()` to maintain proper line/column state\n    const lines = splitByLines(value);\n\n    for (let i = 0, il = lines.length - 1; i <= il; i++) {\n        push(stream, lines[i]);\n        if (i !== il) {\n            pushNewline(stream, true);\n        }\n    }\n}\n\n/**\n * Pushes new line into given output stream\n */\nexport function pushNewline(stream: OutputStream, indent?: boolean | number) {\n    const baseIndent = stream.options['output.baseIndent'];\n    const newline = stream.options['output.newline'];\n    push(stream, newline + baseIndent);\n    stream.line++;\n    stream.column = baseIndent.length;\n    if (indent) {\n        pushIndent(stream, indent === true ? stream.level : indent);\n    }\n}\n\n/**\n * Adds indentation of `size` to current output stream\n */\nexport function pushIndent(stream: OutputStream, size = stream.level) {\n    const indent = stream.options['output.indent'];\n    push(stream, indent.repeat(Math.max(size, 0)));\n}\n\n/**\n * Pushes field/tabstop into output stream\n */\nexport function pushField(stream: OutputStream, index: number, placeholder: string) {\n    const field = stream.options['output.field'];\n    // NB: use `_push` instead of `push` to skip text processing\n    _push(stream, field(index, placeholder, stream.offset, stream.line, stream.column));\n}\n\n/**\n * Returns given tag name formatted according to given config\n */\nexport function tagName(name: string, config: Config) {\n    return strCase(name, config.options['output.tagCase']);\n}\n\n/**\n * Returns given attribute name formatted according to given config\n */\nexport function attrName(name: string, config: Config) {\n    return strCase(name, config.options['output.attributeCase']);\n}\n\n/**\n * Returns character for quoting value of given attribute\n */\nexport function attrQuote(attr: AbbreviationAttribute, config: Config, isOpen?: boolean): string {\n    if (attr.valueType === 'expression') {\n        return isOpen ? expressionStart : expressionEnd;\n    }\n\n    return config.options['output.attributeQuotes'] === 'single' ? '\\'' : '\"';\n}\n\n/**\n * Check if given attribute is boolean\n */\nexport function isBooleanAttribute(attr: AbbreviationAttribute, config: Config): boolean {\n    return attr.boolean\n        || config.options['output.booleanAttributes'].includes((attr.name || '').toLowerCase());\n}\n\n/**\n * Returns a token for self-closing tag, depending on current options\n */\nexport function selfClose(config: Config): string {\n    switch (config.options['output.selfClosingStyle']) {\n        case 'xhtml': return ' /';\n        case 'xml': return '/';\n        default: return '';\n    }\n}\n\n/**\n * Check if given tag name belongs to inline-level element\n * @param node Parsed node or tag name\n */\nexport function isInline(node: string | AbbreviationNode, config: Config): boolean {\n    if (typeof node === 'string') {\n        return config.options.inlineElements.includes(node.toLowerCase());\n    }\n\n    // inline node is a node either with inline-level name or text-only node\n    return node.name ? isInline(node.name, config) : Boolean(node.value && !node.attributes);\n}\n\n/**\n * Splits given text by lines\n */\nexport function splitByLines(text: string): string[] {\n    return text.split(/\\r\\n|\\r|\\n/g);\n}\n\n/**\n * Pushes raw string into output stream without any processing\n */\nfunction _push(stream: OutputStream, text: string) {\n    stream.value += text;\n    stream.offset += text.length;\n    stream.column += text.length;\n}\n\nfunction strCase(str: string, type: StringCase) {\n    if (type) {\n        return type === 'upper' ? str.toUpperCase() : str.toLowerCase();\n    }\n\n    return str;\n}\n"
  },
  {
    "path": "src/snippets/css.json",
    "content": "{\n\t\"@f\": \"@font-face {\\n\\tfont-family: ${1};\\n\\tsrc: url(${2});\\n}\",\n\t\"@ff\": \"@font-face {\\n\\tfont-family: '${1:FontName}';\\n\\tsrc: url('${2:FileName}.eot');\\n\\tsrc: url('${2:FileName}.eot?#iefix') format('embedded-opentype'),\\n\\t\\t url('${2:FileName}.woff') format('woff'),\\n\\t\\t url('${2:FileName}.ttf') format('truetype'),\\n\\t\\t url('${2:FileName}.svg#${1:FontName}') format('svg');\\n\\tfont-style: ${3:normal};\\n\\tfont-weight: ${4:normal};\\n}\",\n\t\"@i|@import\": \"@import url(${0});\",\n\t\"@kf\": \"@keyframes ${1:identifier} {\\n\\t${2}\\n}\",\n\t\"@m|@media\": \"@media ${1:screen} {\\n\\t${0}\\n}\",\n\t\"ac\": \"align-content:start|end|flex-start|flex-end|center|space-between|space-around|stretch|space-evenly\",\n\t\"ai\": \"align-items:start|end|flex-start|flex-end|center|baseline|stretch\",\n\t\"anim\": \"animation:${1:name} ${2:duration} ${3:timing-function} ${4:delay} ${5:iteration-count} ${6:direction} ${7:fill-mode}\",\n\t\"animdel\": \"animation-delay:time\",\n\t\"animdir\": \"animation-direction:normal|reverse|alternate|alternate-reverse\",\n\t\"animdur\": \"animation-duration:${1:0}s\",\n\t\"animfm\": \"animation-fill-mode:both|forwards|backwards\",\n\t\"animic\": \"animation-iteration-count:1|infinite\",\n\t\"animn\": \"animation-name\",\n\t\"animps\": \"animation-play-state:running|paused\",\n\t\"animtf\": \"animation-timing-function:linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(${1:0.1}, ${2:0.7}, ${3:1.0}, ${3:0.1})\",\n\t\"ap\": \"appearance:none\",\n\t\"ar\": \"aspect-ratio\",\n\t\"as\": \"align-self:start|end|auto|flex-start|flex-end|center|baseline|stretch\",\n\t\"b\": \"bottom\",\n\t\"bd\": \"border:${1:1px} ${2:solid} ${3:#000}\",\n\t\"bdb\": \"border-bottom:${1:1px} ${2:solid} ${3:#000}\",\n\t\"bdbc\": \"border-bottom-color:${1:#000}\",\n\t\"bdbi\": \"border-bottom-image:url(${0})\",\n\t\"bdbk\": \"border-break:close\",\n\t\"bdbli\": \"border-bottom-left-image:url(${0})|continue\",\n\t\"bdblrs\": \"border-bottom-left-radius\",\n\t\"bdbri\": \"border-bottom-right-image:url(${0})|continue\",\n\t\"bdbrrs\": \"border-bottom-right-radius\",\n\t\"bdbs\": \"border-bottom-style\",\n\t\"bdbw\": \"border-bottom-width\",\n\t\"bdc\": \"border-color:${1:#000}\",\n\t\"bdci\": \"border-corner-image:url(${0})|continue\",\n\t\"bdcl\": \"border-collapse:collapse|separate\",\n\t\"bdf\": \"border-fit:repeat|clip|scale|stretch|overwrite|overflow|space\",\n\t\"bdi\": \"border-image:url(${0})\",\n\t\"bdl\": \"border-left:${1:1px} ${2:solid} ${3:#000}\",\n\t\"bdlc\": \"border-left-color:${1:#000}\",\n\t\"bdlen\": \"border-length\",\n\t\"bdli\": \"border-left-image:url(${0})\",\n\t\"bdls\": \"border-left-style\",\n\t\"bdlw\": \"border-left-width\",\n\t\"bdr\": \"border-right:${1:1px} ${2:solid} ${3:#000}\",\n\t\"bdrc\": \"border-right-color:${1:#000}\",\n\t\"bdri\": \"border-right-image:url(${0})\",\n\t\"bdrs\": \"border-radius\",\n\t\"bdrst\": \"border-right-style\",\n\t\"bdrw\": \"border-right-width\",\n\t\"bds\": \"border-style:none|hidden|dotted|dashed|solid|double|dot-dash|dot-dot-dash|wave|groove|ridge|inset|outset\",\n\t\"bdsp\": \"border-spacing\",\n\t\"bdt\": \"border-top:${1:1px} ${2:solid} ${3:#000}\",\n\t\"bdtc\": \"border-top-color:${1:#000}\",\n\t\"bdti\": \"border-top-image:url(${0})\",\n\t\"bdtli\": \"border-top-left-image:url(${0})|continue\",\n\t\"bdtlrs\": \"border-top-left-radius\",\n\t\"bdtri\": \"border-top-right-image:url(${0})|continue\",\n\t\"bdtrrs\": \"border-top-right-radius\",\n\t\"bdts\": \"border-top-style\",\n\t\"bdtw\": \"border-top-width\",\n\t\"bdw\": \"border-width\",\n  \"bdbl\": \"border-block\",\n  \"bdin\": \"border-inline\",\n  \"bbs|bdbls\": \"border-block-start\",\n  \"bbe|bdble\": \"border-block-end\",\n  \"bdis\": \"border-inline-start\",\n  \"bdie\": \"border-inline-end\",\n\t\"bfv\": \"backface-visibility:hidden|visible\",\n\t\"bg\": \"background:${1:#000}\",\n\t\"bg:n\": \"background: none\",\n\t\"bga\": \"background-attachment:fixed|scroll\",\n\t\"bgbk\": \"background-break:bounding-box|each-box|continuous\",\n\t\"bgc\": \"background-color:${1:#fff}\",\n\t\"bgcp\": \"background-clip:padding-box|border-box|content-box|no-clip\",\n\t\"bgi\": \"background-image:url(${0})\",\n\t\"bgo\": \"background-origin:padding-box|border-box|content-box\",\n\t\"bgp\": \"background-position:${1:0} ${2:0}\",\n\t\"bgpx\": \"background-position-x\",\n\t\"bgpy\": \"background-position-y\",\n\t\"bgr\": \"background-repeat:no-repeat|repeat-x|repeat-y|space|round\",\n\t\"bgsz\": \"background-size:contain|cover\",\n  \"bs\": \"block-size\",\n\t\"bxsh\": \"box-shadow:${1:inset }${2:hoff} ${3:voff} ${4:blur} ${5:#000}|none\",\n\t\"bxsz\": \"box-sizing:border-box|content-box|border-box\",\n\t\"c\": \"color:${1:#000}\",\n  \"cg\": \"column-gap\",\n\t\"cr\": \"color:rgb(${1:0}, ${2:0}, ${3:0})\",\n\t\"cra\": \"color:rgba(${1:0}, ${2:0}, ${3:0}, ${4:.5})\",\n\t\"cl\": \"clear:both|left|right|none\",\n\t\"cm\": \"/* ${0} */\",\n\t\"cnt\": \"content:'${0}'|normal|open-quote|no-open-quote|close-quote|no-close-quote|attr(${0})|counter(${0})|counters(${0})\",\n\t\"coi\": \"counter-increment\",\n\t\"colm\": \"columns\",\n\t\"colmc\": \"column-count\",\n\t\"colmf\": \"column-fill\",\n\t\"colmg\": \"column-gap\",\n\t\"colmr\": \"column-rule\",\n\t\"colmrc\": \"column-rule-color\",\n\t\"colmrs\": \"column-rule-style\",\n\t\"colmrw\": \"column-rule-width\",\n\t\"colms\": \"column-span\",\n\t\"colmw\": \"column-width\",\n\t\"cor\": \"counter-reset\",\n\t\"cp\": \"clip:auto|rect(${1:top} ${2:right} ${3:bottom} ${4:left})\",\n\t\"cps\": \"caption-side:top|bottom\",\n\t\"cur\": \"cursor:pointer|auto|default|crosshair|hand|help|move|pointer|text\",\n\t\"d\": \"display:block|none|flex|inline-flex|inline|inline-block|grid|inline-grid|subgrid|list-item|run-in|contents|table|inline-table|table-caption|table-column|table-column-group|table-header-group|table-footer-group|table-row|table-row-group|table-cell|ruby|ruby-base|ruby-base-group|ruby-text|ruby-text-group\",\n\t\"ec\": \"empty-cells:show|hide\",\n\t\"f\": \"font:${1:1em} ${2:sans-serif}\",\n\t\"fd\": \"font-display:auto|block|swap|fallback|optional\",\n\t\"fef\": \"font-effect:none|engrave|emboss|outline\",\n\t\"fem\": \"font-emphasize\",\n\t\"femp\": \"font-emphasize-position:before|after\",\n\t\"fems\": \"font-emphasize-style:none|accent|dot|circle|disc\",\n\t\"ff\": \"font-family:serif|sans-serif|cursive|fantasy|monospace\",\n\t\"fft\": \"font-family:\\\"Times New Roman\\\", Times, Baskerville, Georgia, serif\",\n\t\"ffa\": \"font-family:Arial, \\\"Helvetica Neue\\\", Helvetica, sans-serif\",\n\t\"ffv\": \"font-family:Verdana, Geneva, sans-serif\",\n\t\"fl\": \"float:left|right|none\",\n\t\"fs\": \"font-style:italic|normal|oblique\",\n\t\"fsm\": \"font-smoothing:antialiased|subpixel-antialiased|none\",\n\t\"fst\": \"font-stretch:normal|ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded\",\n\t\"fv\": \"font-variant:normal|small-caps\",\n\t\"fvs\": \"font-variation-settings:normal|inherit|initial|unset\",\n\t\"fw\": \"font-weight:normal|bold|bolder|lighter\",\n\t\"fx\": \"flex\",\n\t\"fxb\": \"flex-basis:fill|max-content|min-content|fit-content|content\",\n\t\"fxd\": \"flex-direction:row|row-reverse|column|column-reverse\",\n\t\"fxf\": \"flex-flow\",\n\t\"fxg\": \"flex-grow\",\n\t\"fxsh\": \"flex-shrink\",\n\t\"fxw\": \"flex-wrap:nowrap|wrap|wrap-reverse\",\n\t\"fsz\": \"font-size\",\n\t\"fsza\": \"font-size-adjust\",\n\t\"g\": \"gap\",\n\t\"gtc\": \"grid-template-columns:repeat(${0})|minmax()\",\n\t\"gtr\": \"grid-template-rows:repeat(${0})|minmax()\",\n\t\"gta\": \"grid-template-areas\",\n\t\"gt\": \"grid-template\",\n\t\"gg\": \"grid-gap\",\n\t\"gcg\": \"grid-column-gap\",\n\t\"grg\": \"grid-row-gap\",\n\t\"gac\": \"grid-auto-columns:auto|minmax()\",\n\t\"gar\": \"grid-auto-rows:auto|minmax()\",\n\t\"gaf\": \"grid-auto-flow:row|column|dense|inherit|initial|unset\",\n\t\"gd\": \"grid\",\n\t\"gc\": \"grid-column\",\n\t\"gcs\": \"grid-column-start\",\n\t\"gce\": \"grid-column-end\",\n\t\"gr\": \"grid-row\",\n\t\"grs\": \"grid-row-start\",\n\t\"gre\": \"grid-row-end\",\n\t\"ga\": \"grid-area\",\n\t\"h\": \"height\",\n\t\"i\": \"inset\",\n\t\"ib\": \"inset-block\",\n\t\"ii\": \"inset-inline\",\n\t\"ibs\": \"inset-block-start\",\n\t\"ibe\": \"inset-block-end\",\n\t\"iis\": \"inset-inline-start\",\n\t\"iie\": \"inset-inline-end\",\n  \"is\": \"inline-size\",\n\t\"jc\": \"justify-content:start|end|stretch|flex-start|flex-end|center|space-between|space-around|space-evenly\",\n\t\"ji\": \"justify-items:start|end|center|stretch\",\n\t\"js\": \"justify-self:start|end|center|stretch\",\n\t\"l\": \"left\",\n\t\"lg\": \"background-image:linear-gradient(${1})\",\n\t\"lh\": \"line-height\",\n\t\"lis\": \"list-style\",\n\t\"lisi\": \"list-style-image\",\n\t\"lisp\": \"list-style-position:inside|outside\",\n\t\"list\": \"list-style-type:disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman\",\n\t\"lts\": \"letter-spacing:normal\",\n\t\"m\": \"margin\",\n\t\"mah\": \"max-height\",\n\t\"mar\": \"max-resolution\",\n\t\"maw\": \"max-width\",\n\t\"mab\": \"max-block-size\",\n\t\"mai\": \"max-inline-size\",\n\t\"mb\": \"margin-bottom\",\n\t\"mih\": \"min-height\",\n\t\"mir\": \"min-resolution\",\n\t\"miw\": \"min-width\",\n\t\"mib\": \"min-block-size\",\n\t\"mii\": \"min-inline-size\",\n\t\"ml\": \"margin-left\",\n\t\"mr\": \"margin-right\",\n\t\"mt\": \"margin-top\",\n\t\"mbl\": \"margin-block\",\n\t\"mi\": \"margin-inline\",\n  \"mbs\": \"margin-block-start\",\n  \"mbe\": \"margin-block-end\",\n  \"mis\": \"margin-inline-start\",\n  \"mie\": \"margin-inline-end\",\n\t\"ol\": \"outline\",\n\t\"olc\": \"outline-color:${1:#000}|invert\",\n\t\"olo\": \"outline-offset\",\n\t\"ols\": \"outline-style:none|dotted|dashed|solid|double|groove|ridge|inset|outset\",\n\t\"olw\": \"outline-width:thin|medium|thick\",\n\t\"op|opa\": \"opacity\",\n\t\"ord\": \"order\",\n\t\"ori\": \"orientation:landscape|portrait\",\n\t\"orp\": \"orphans\",\n\t\"ov\": \"overflow:hidden|visible|hidden|scroll|auto\",\n\t\"ovs\": \"overflow-style:scrollbar|auto|scrollbar|panner|move|marquee\",\n\t\"ovx\": \"overflow-x:hidden|visible|hidden|scroll|auto\",\n\t\"ovy\": \"overflow-y:hidden|visible|hidden|scroll|auto\",\n\t\"p\": \"padding\",\n\t\"pb\": \"padding-bottom\",\n\t\"pgba\": \"page-break-after:auto|always|left|right\",\n\t\"pgbb\": \"page-break-before:auto|always|left|right\",\n\t\"pgbi\": \"page-break-inside:auto|avoid\",\n\t\"pl\": \"padding-left\",\n\t\"pos\": \"position:relative|absolute|relative|fixed|static|sticky\",\n\t\"pr\": \"padding-right\",\n\t\"pt\": \"padding-top\",\n\t\"pbl\": \"padding-block\",\n\t\"pi\": \"padding-inline\",\n  \"pbs\": \"padding-block-start\",\n  \"pbe\": \"padding-block-end\",\n  \"pis\": \"padding-inline-start\",\n  \"pie\": \"padding-inline-end\",\n  \"sb\": \"scroll-behavior\",\n  \"spbs\": \"scroll-padding-block-start\",\n  \"spbe\": \"scroll-padding-block-end\",\n  \"spis\": \"scroll-padding-inline-start\",\n  \"spie\": \"scroll-padding-inline-end\",\n\t\"q\": \"quotes\",\n\t\"qen\": \"quotes:'\\\\201C' '\\\\201D' '\\\\2018' '\\\\2019'\",\n\t\"qru\": \"quotes:'\\\\00AB' '\\\\00BB' '\\\\201E' '\\\\201C'\",\n\t\"r\": \"right\",\n  \"rg\": \"row-gap\",\n\t\"rsz\": \"resize:none|both|horizontal|vertical\",\n\t\"t\": \"top\",\n\t\"ta\": \"text-align:left|center|right|justify\",\n\t\"tal\": \"text-align-last:left|center|right\",\n\t\"tbl\": \"table-layout:fixed\",\n\t\"td\": \"text-decoration:none|underline|overline|line-through\",\n\t\"te\": \"text-emphasis:none|accent|dot|circle|disc|before|after\",\n\t\"th\": \"text-height:auto|font-size|text-size|max-size\",\n\t\"ti\": \"text-indent\",\n\t\"tj\": \"text-justify:auto|inter-word|inter-ideograph|inter-cluster|distribute|kashida|tibetan\",\n\t\"to\": \"text-outline:${1:0} ${2:0} ${3:#000}\",\n\t\"tov\": \"text-overflow:ellipsis|clip\",\n\t\"tr\": \"text-replace\",\n\t\"trf\": \"transform:${1}|skewX(${1:angle})|skewY(${1:angle})|scale(${1:x}, ${2:y})|scaleX(${1:x})|scaleY(${1:y})|scaleZ(${1:z})|scale3d(${1:x}, ${2:y}, ${3:z})|rotate(${1:angle})|rotateX(${1:angle})|rotateY(${1:angle})|rotateZ(${1:angle})|translate(${1:x}, ${2:y})|translateX(${1:x})|translateY(${1:y})|translateZ(${1:z})|translate3d(${1:tx}, ${2:ty}, ${3:tz})\",\n\t\"trfo\": \"transform-origin\",\n\t\"trfs\": \"transform-style:preserve-3d\",\n\t\"trs\": \"transition:${1:prop} ${2:time}\",\n\t\"trsde\": \"transition-delay:${1:time}\",\n\t\"trsdu\": \"transition-duration:${1:time}\",\n\t\"trsp\": \"transition-property:${1:prop}\",\n\t\"trstf\": \"transition-timing-function:${1:fn}\",\n\t\"tsh\": \"text-shadow:${1:hoff} ${2:voff} ${3:blur} ${4:#000}\",\n\t\"tt\": \"text-transform:uppercase|lowercase|capitalize|none\",\n\t\"tw\": \"text-wrap:none|normal|unrestricted|suppress\",\n\t\"us\": \"user-select:none\",\n\t\"v\": \"visibility:hidden|visible|collapse\",\n\t\"va\": \"vertical-align:top|super|text-top|middle|baseline|bottom|text-bottom|sub\",\n\t\"w|wid\": \"width\",\n\t\"whs\": \"white-space:nowrap|pre|pre-wrap|pre-line|normal\",\n\t\"whsc\": \"white-space-collapse:normal|keep-all|loose|break-strict|break-all\",\n\t\"wido\": \"widows\",\n\t\"wm\": \"writing-mode:lr-tb|lr-tb|lr-bt|rl-tb|rl-bt|tb-rl|tb-lr|bt-lr|bt-rl\",\n\t\"wob\": \"word-break:normal|keep-all|break-all\",\n\t\"wos\": \"word-spacing\",\n\t\"wow\": \"word-wrap:none|unrestricted|suppress|break-word|normal\",\n\t\"z\": \"z-index\",\n\t\"zom\": \"zoom:1\"\n}\n"
  },
  {
    "path": "src/snippets/html.json",
    "content": "{\n\t\"a\": \"a[href]\",\n\t\"a:blank\": \"a[href='http://${0}' target='_blank' rel='noopener noreferrer']\",\n\t\"a:link\": \"a[href='http://${0}']\",\n\t\"a:mail\": \"a[href='mailto:${0}']\",\n\t\"a:tel\": \"a[href='tel:+${0}']\",\n\t\"abbr\": \"abbr[title]\",\n\t\"acr|acronym\": \"acronym[title]\",\n\t\"base\": \"base[href]/\",\n\t\"basefont\": \"basefont/\",\n\t\"br\": \"br/\",\n\t\"frame\": \"frame/\",\n\t\"hr\": \"hr/\",\n\t\"bdo\": \"bdo[dir]\",\n\t\"bdo:r\": \"bdo[dir=rtl]\",\n\t\"bdo:l\": \"bdo[dir=ltr]\",\n\t\"col\": \"col/\",\n\t\"link\": \"link[rel=stylesheet href]/\",\n\t\"link:css\": \"link[href='${1:style}.css']\",\n\t\"link:print\": \"link[href='${1:print}.css' media=print]\",\n\t\"link:favicon\": \"link[rel='icon' type=image/x-icon href='${1:favicon.ico}']\",\n\t\"link:mf|link:manifest\": \"link[rel='manifest' href='${1:manifest.json}']\",\n\t\"link:touch\": \"link[rel=apple-touch-icon href='${1:favicon.png}']\",\n\t\"link:rss\": \"link[rel=alternate type=application/rss+xml title=RSS href='${1:rss.xml}']\",\n\t\"link:atom\": \"link[rel=alternate type=application/atom+xml title=Atom href='${1:atom.xml}']\",\n\t\"link:im|link:import\": \"link[rel=import href='${1:component}.html']\",\n  \"link:preload\": \"link[rel=preload href='${1}' as='${2}']/\",\n\t\"meta\": \"meta/\",\n\t\"meta:utf\": \"meta[http-equiv=Content-Type content='text/html;charset=UTF-8']\",\n\t\"meta:vp\": \"meta[name=viewport content='width=${1:device-width}, initial-scale=${2:1.0}']\",\n\t\"meta:compat\": \"meta[http-equiv=X-UA-Compatible content='${1:IE=7}']\",\n\t\"meta:edge\": \"meta:compat[content='${1:ie=edge}']\",\n\t\"meta:redirect\": \"meta[http-equiv=refresh content='0; url=${1:http://example.com}']\",\n\t\"meta:refresh\": \"meta[http-equiv=refresh content='${1:5}']\",\n\t\"meta:kw\": \"meta[name=keywords content]\",\n\t\"meta:desc\": \"meta[name=description content]\",\n\t\"style\": \"style\",\n\t\"script\": \"script\",\n\t\"script:src\": \"script[src]\",\n\t\"script:module\": \"script[type=module src]\",\n\t\"img\": \"img[src alt]/\",\n\t\"img:s|img:srcset\": \"img[srcset src alt]\",\n\t\"img:z|img:sizes\": \"img[sizes srcset src alt]\",\n\t\"picture\": \"picture\",\n\t\"src|source\": \"source/\",\n\t\"src:sc|source:src\": \"source[src type]\",\n\t\"src:s|source:srcset\": \"source[srcset]\",\n\t\"src:t|source:type\": \"source[srcset type='${1:image/}']\",\n\t\"src:z|source:sizes\": \"source[sizes srcset]\",\n\t\"src:m|source:media\": \"source[media='(${1:min-width: })' srcset]\",\n\t\"src:mt|source:media:type\": \"source:media[type='${2:image/}']\",\n\t\"src:mz|source:media:sizes\": \"source:media[sizes srcset]\",\n\t\"src:zt|source:sizes:type\": \"source[sizes srcset type='${1:image/}']\",\n\t\"iframe\": \"iframe[src frameborder=0]\",\n\t\"embed\": \"embed[src type]/\",\n\t\"object\": \"object[data type]\",\n\t\"param\": \"param[name value]/\",\n\t\"map\": \"map[name]\",\n\t\"area\": \"area[shape coords href alt]/\",\n\t\"area:d\": \"area[shape=default]\",\n\t\"area:c\": \"area[shape=circle]\",\n\t\"area:r\": \"area[shape=rect]\",\n\t\"area:p\": \"area[shape=poly]\",\n\t\"form\": \"form[action]\",\n\t\"form:get\": \"form[method=get]\",\n\t\"form:post\": \"form[method=post]\",\n\t\"label\": \"label[for]\",\n\t\"input\": \"input[type=${1:text}]/\",\n\t\"inp\": \"input[name=${1} id=${1}]\",\n\t\"input:h|input:hidden\": \"input[type=hidden name]\",\n\t\"input:t|input:text\": \"inp[type=text]\",\n\t\"input:search\": \"inp[type=search]\",\n\t\"input:email\": \"inp[type=email]\",\n\t\"input:url\": \"inp[type=url]\",\n\t\"input:p|input:password\": \"inp[type=password]\",\n\t\"input:datetime\": \"inp[type=datetime]\",\n\t\"input:date\": \"inp[type=date]\",\n\t\"input:datetime-local\": \"inp[type=datetime-local]\",\n\t\"input:month\": \"inp[type=month]\",\n\t\"input:week\": \"inp[type=week]\",\n\t\"input:time\": \"inp[type=time]\",\n\t\"input:tel\": \"inp[type=tel]\",\n\t\"input:number\": \"inp[type=number]\",\n\t\"input:color\": \"inp[type=color]\",\n\t\"input:c|input:checkbox\": \"inp[type=checkbox]\",\n\t\"input:r|input:radio\": \"inp[type=radio]\",\n\t\"input:range\": \"inp[type=range]\",\n\t\"input:f|input:file\": \"inp[type=file]\",\n\t\"input:s|input:submit\": \"input[type=submit value]\",\n\t\"input:i|input:image\": \"input[type=image src alt]\",\n\t\"input:b|input:btn|input:button\": \"input[type=button value]\",\n\t\"input:reset\": \"input:button[type=reset]\",\n\t\"isindex\": \"isindex/\",\n\t\"select\": \"select[name=${1} id=${1}]\",\n\t\"select:d|select:disabled\": \"select[disabled.]\",\n\t\"opt|option\": \"option[value]\",\n\t\"textarea\": \"textarea[name=${1} id=${1}]\",\n\t\"tarea:c|textarea:cols\":\"textarea[name=${1} id=${1} cols=${2:30}]\",\n\t\"tarea:r|textarea:rows\":\"textarea[name=${1} id=${1} rows=${3:10}]\",\n\t\"tarea:cr|textarea:cols:rows\":\"textarea[name=${1} id=${1} cols=${2:30} rows=${3:10}]\",\n\t\"marquee\": \"marquee[behavior direction]\",\n\t\"menu:c|menu:context\": \"menu[type=context]\",\n\t\"menu:t|menu:toolbar\": \"menu[type=toolbar]\",\n\t\"video\": \"video[src]\",\n\t\"audio\": \"audio[src]\",\n\t\"html:xml\": \"html[xmlns=http://www.w3.org/1999/xhtml]\",\n\t\"keygen\": \"keygen/\",\n\t\"command\": \"command/\",\n\t\"btn:s|button:s|button:submit\" : \"button[type=submit]\",\n\t\"btn:r|button:r|button:reset\" : \"button[type=reset]\",\n\t\"btn:b|button:b|button:button\" : \"button[type=button]\",\n\t\"btn:d|button:d|button:disabled\" : \"button[disabled.]\",\n\t\"fst:d|fset:d|fieldset:d|fieldset:disabled\" : \"fieldset[disabled.]\",\n\n\t\"bq\": \"blockquote\",\n\t\"fig\": \"figure\",\n\t\"figc\": \"figcaption\",\n\t\"pic\": \"picture\",\n\t\"ifr\": \"iframe\",\n\t\"emb\": \"embed\",\n\t\"obj\": \"object\",\n\t\"cap\": \"caption\",\n\t\"colg\": \"colgroup\",\n\t\"fst\": \"fieldset\",\n\t\"btn\": \"button\",\n\t\"optg\": \"optgroup\",\n\t\"tarea\": \"textarea\",\n\t\"leg\": \"legend\",\n\t\"sect\": \"section\",\n\t\"art\": \"article\",\n\t\"hdr\": \"header\",\n\t\"ftr\": \"footer\",\n\t\"adr\": \"address\",\n\t\"dlg\": \"dialog\",\n\t\"str\": \"strong\",\n\t\"prog\": \"progress\",\n\t\"mn\": \"main\",\n\t\"tem\": \"template\",\n\t\"fset\": \"fieldset\",\n\t\"datal\": \"datalist\",\n\t\"kg\": \"keygen\",\n\t\"out\": \"output\",\n\t\"det\": \"details\",\n\t\"sum\": \"summary\",\n\t\"cmd\": \"command\",\n\t\"data\": \"data[value]\",\n\t\"meter\": \"meter[value]\",\n\t\"time\": \"time[datetime]\",\n\n\t\"ri:d|ri:dpr\": \"img:s\",\n\t\"ri:v|ri:viewport\": \"img:z\",\n\t\"ri:a|ri:art\": \"pic>src:m+img\",\n\t\"ri:t|ri:type\": \"pic>src:t+img\",\n\n\t\"!!!\": \"{<!DOCTYPE html>}\",\n\t\"doc\": \"html[lang=${lang}]>(head>meta[charset=${charset}]+meta:vp+title{${1:Document}})+body\",\n\t\"!|html:5\": \"!!!+doc\",\n\n\t\"c\": \"{<!-- ${0} -->}\",\n\t\"cc:ie\": \"{<!--[if IE]>${0}<![endif]-->}\",\n\t\"cc:noie\": \"{<!--[if !IE]><!-->${0}<!--<![endif]-->}\"\n}\n"
  },
  {
    "path": "src/snippets/pug.json",
    "content": "{\n\t\"!!!\": \"{doctype html}\"\n}\n"
  },
  {
    "path": "src/snippets/variables.json",
    "content": "{\n\t\"lang\": \"en\",\n\t\"locale\": \"en-US\",\n\t\"charset\": \"utf-8\",\n\t\"indentation\": \"\\t\",\n\t\"newline\": \"\\n\"\n}\n"
  },
  {
    "path": "src/snippets/xsl.json",
    "content": "{\n    \"tm|tmatch\": \"xsl:template[match mode]\",\n    \"tn|tname\": \"xsl:template[name]\",\n    \"call\": \"xsl:call-template[name]\",\n    \"ap\": \"xsl:apply-templates[select mode]\",\n    \"api\": \"xsl:apply-imports\",\n    \"imp\": \"xsl:import[href]\",\n    \"inc\": \"xsl:include[href]\",\n    \"ch\": \"xsl:choose\",\n    \"wh|xsl:when\": \"xsl:when[test]\",\n    \"ot\": \"xsl:otherwise\",\n    \"if\": \"xsl:if[test]\",\n    \"par\": \"xsl:param[name]\",\n    \"pare\": \"xsl:param[name select]\",\n    \"var\": \"xsl:variable[name]\",\n    \"vare\": \"xsl:variable[name select]\",\n    \"wp\": \"xsl:with-param[name select]\",\n    \"key\": \"xsl:key[name match use]\",\n    \"elem\": \"xsl:element[name]\",\n    \"attr\": \"xsl:attribute[name]\",\n    \"attrs\": \"xsl:attribute-set[name]\",\n    \"cp\": \"xsl:copy[select]\",\n    \"co\": \"xsl:copy-of[select]\",\n    \"val\": \"xsl:value-of[select]\",\n    \"for|each\": \"xsl:for-each[select]\",\n    \"tex\": \"xsl:text\",\n    \"com\": \"xsl:comment\",\n    \"msg\": \"xsl:message[terminate=no]\",\n    \"fall\": \"xsl:fallback\",\n    \"num\": \"xsl:number[value]\",\n    \"nam\": \"namespace-alias[stylesheet-prefix result-prefix]\",\n    \"pres\": \"xsl:preserve-space[elements]\",\n    \"strip\": \"xsl:strip-space[elements]\",\n    \"proc\": \"xsl:processing-instruction[name]\",\n    \"sort\": \"xsl:sort[select order]\",\n    \"choose\": \"xsl:choose>xsl:when+xsl:otherwise\",\n    \"xsl\": \"!!!+xsl:stylesheet[version=1.0 xmlns:xsl=http://www.w3.org/1999/XSL/Transform]>{\\n|}\",\n    \"!!!\": \"{<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>}\"\n}"
  },
  {
    "path": "src/stylesheet/color.ts",
    "content": "import type { ColorValue } from '@emmetio/css-abbreviation';\n\nexport default function color(token: ColorValue, shortHex?: boolean): string {\n    if (!token.r && !token.g && !token.b && !token.a) {\n        return 'transparent';\n    } else if (token.a === 1) {\n        return asHex(token, shortHex);\n    }\n\n    return asRGB(token);\n}\n\n/**\n * Output given color as hex value\n * @param short Produce short value (e.g. #fff instead of #ffffff), if possible\n */\nexport function asHex(token: ColorValue, short?: boolean): string {\n    const fn = (short && isShortHex(token.r) && isShortHex(token.g) && isShortHex(token.b))\n        ? toShortHex : toHex;\n\n    return '#' + fn(token.r) + fn(token.g) + fn(token.b);\n}\n\n/**\n * Output current color as `rgba?(...)` CSS color\n */\nfunction asRGB(token: ColorValue): string {\n    const values: Array<string | number> = [token.r, token.g, token.b];\n    if (token.a !== 1) {\n        values.push(frac(token.a, 8));\n    }\n\n    return `${values.length === 3 ? 'rgb' : 'rgba'}(${values.join(', ')})`;\n}\n\nexport function frac(num: number, digits = 4): string {\n    return num.toFixed(digits).replace(/\\.?0+$/, '');\n}\n\nfunction isShortHex(hex: number): boolean {\n    return !(hex % 17);\n}\n\nfunction toShortHex(num: number): string {\n    return (num >> 4).toString(16);\n}\n\nfunction toHex(num: number): string {\n    return pad(num.toString(16), 2);\n}\n\nfunction pad(value: string, len: number): string {\n    while (value.length < len) {\n        value = '0' + value;\n    }\n    return value;\n}\n"
  },
  {
    "path": "src/stylesheet/format.ts",
    "content": "import type { CSSAbbreviation, CSSProperty, Value, CSSValue, NumberValue } from '@emmetio/css-abbreviation';\nimport createOutputStream, { type OutputStream, push, pushString, pushField, pushNewline } from '../output-stream';\nimport type { Config } from '../config';\nimport color, { frac } from './color';\n\nexport const CSSAbbreviationScope = {\n    /** Include all possible snippets in match */\n    Global: '@@global',\n    /** Include raw snippets only (e.g. no properties) in abbreviation match */\n    Section: '@@section',\n    /** Include properties only in abbreviation match */\n    Property: '@@property',\n    /** Resolve abbreviation in context of CSS property value */\n    Value: '@@value',\n} as const;\n\nexport default function css(abbr: CSSAbbreviation, config: Config): string {\n    const out = createOutputStream(config.options);\n    const format = config.options['output.format'];\n\n    if (config.context?.name === CSSAbbreviationScope.Section) {\n        // For section context, filter out unmatched snippets\n        abbr = abbr.filter(node => node.snippet);\n    }\n\n    for (let i = 0; i < abbr.length; i++) {\n        if (format && i !== 0) {\n            pushNewline(out, true);\n        }\n        property(abbr[i], out, config);\n    }\n\n    return out.value;\n}\n\n/**\n * Outputs given abbreviation node into output stream\n */\nfunction property(node: CSSProperty, out: OutputStream, config: Config) {\n    const isJSON = config.options['stylesheet.json'];\n    if (node.name) {\n        // It’s a CSS property\n        const name = isJSON ? toCamelCase(node.name) : node.name;\n        pushString(out, name + config.options['stylesheet.between']);\n\n        if (node.value.length) {\n            propertyValue(node, out, config);\n        } else {\n            pushField(out, 0, '');\n        }\n\n        if (isJSON) {\n            // For CSS-in-JS, always finalize property with comma\n            // NB: seems like `important` is not available in CSS-in-JS syntaxes\n            push(out, ',');\n        } else {\n            outputImportant(node, out, true);\n            push(out, config.options['stylesheet.after']);\n        }\n    } else {\n        // It’s a regular snippet, output plain tokens without any additional formatting\n        for (const cssVal of node.value) {\n            for (const v of cssVal.value) {\n                outputToken(v, out, config);\n            }\n        }\n        outputImportant(node, out, node.value.length > 0);\n    }\n}\n\nfunction propertyValue(node: CSSProperty, out: OutputStream, config: Config) {\n    const isJSON = config.options['stylesheet.json'];\n    const num = isJSON ? getSingleNumeric(node) : null;\n\n    if (num && (!num.unit || num.unit === 'px')) {\n        // For CSS-in-JS, if property contains single numeric value, output it\n        // as JS number\n        push(out, String(num.value));\n    } else {\n        const quote = getQuote(config);\n        isJSON && push(out, quote);\n        for (let i = 0; i < node.value.length; i++) {\n            if (i !== 0) {\n                push(out, ', ');\n            }\n            outputValue(node.value[i], out, config);\n        }\n        isJSON && push(out, quote);\n    }\n}\n\nfunction outputImportant(node: CSSProperty, out: OutputStream, separator?: boolean) {\n    if (node.important) {\n        if (separator) {\n            push(out, ' ');\n        }\n        push(out, '!important');\n    }\n}\n\nfunction outputValue(value: CSSValue, out: OutputStream, config: Config) {\n    for (let i = 0, prevEnd = -1; i < value.value.length; i++) {\n        const token = value.value[i];\n        // Handle edge case: a field is written close to previous token like this: `foo${bar}`.\n        // We should not add delimiter here\n        if (i !== 0 && (token.type !== 'Field' || token.start !== prevEnd)) {\n            push(out, ' ');\n        }\n\n        outputToken(token, out, config);\n        prevEnd = token['end'];\n    }\n}\n\nfunction outputToken(token: Value, out: OutputStream, config: Config) {\n    if (token.type === 'ColorValue') {\n        push(out, color(token, config.options['stylesheet.shortHex']));\n    } else if (token.type === 'Literal' || token.type === 'CustomProperty') {\n        pushString(out, token.value);\n    } else if (token.type === 'NumberValue') {\n        pushString(out, frac(token.value, 4) + token.unit);\n    } else if (token.type === 'StringValue') {\n        const quote = token.quote === 'double' ? '\"' : '\\'';\n        pushString(out, quote + token.value + quote);\n    } else if (token.type === 'Field') {\n        pushField(out, token.index!, token.name);\n    } else if (token.type === 'FunctionCall') {\n        push(out, token.name + '(');\n        for (let i = 0; i < token.arguments.length; i++) {\n            if (i) {\n                push(out, ', ');\n            }\n            outputValue(token.arguments[i], out, config);\n        }\n        push(out, ')');\n    }\n}\n\n/**\n * If value of given property is a single numeric value, returns this token\n */\nfunction getSingleNumeric(node: CSSProperty): NumberValue | void {\n    if (node.value.length === 1) {\n        const cssVal = node.value[0]!;\n        if (cssVal.value.length === 1 && cssVal.value[0]!.type === 'NumberValue') {\n            return cssVal.value[0] as NumberValue;\n        }\n    }\n}\n\n/**\n * Converts kebab-case string to camelCase\n */\nfunction toCamelCase(str: string): string {\n    return str.replace(/\\-(\\w)/g, (_, letter: string) => letter.toUpperCase());\n}\n\nfunction getQuote(config: Config): string {\n    return config.options['stylesheet.jsonDoubleQuotes'] ? '\"' : '\\'';\n}\n"
  },
  {
    "path": "src/stylesheet/index.ts",
    "content": "import abbreviation from '@emmetio/css-abbreviation';\nimport type { CSSAbbreviation, CSSProperty, CSSValue, Literal, Value, Field, FunctionCall } from '@emmetio/css-abbreviation';\nimport type { Config, SnippetsMap } from '../config';\nimport createSnippet, { nest, CSSSnippetType } from './snippets';\nimport type { CSSSnippet, CSSSnippetRaw, CSSSnippetProperty } from './snippets';\nimport calculateScore from './score';\nimport color from './color';\nimport { CSSAbbreviationScope } from './format';\n\nexport { default as stringify, CSSAbbreviationScope } from './format';\n\ntype MatchInput = CSSSnippet | string;\n\nconst gradientName = 'lg';\n\n/**\n * Parses given Emmet abbreviation into a final abbreviation tree with all\n * required transformations applied\n */\nexport default function parse(abbr: string | CSSAbbreviation, config: Config): CSSAbbreviation {\n    const snippets = config.cache?.stylesheetSnippets || convertSnippets(config.snippets);\n    const result: CSSAbbreviation = []\n\n    if (config.cache) {\n        config.cache.stylesheetSnippets = snippets;\n    }\n\n    if (typeof abbr === 'string') {\n        abbr = abbreviation(abbr, { value: isValueScope(config) });\n    }\n\n    const filteredSnippets = getSnippetsForScope(snippets, config);\n\n    for (const node of abbr) {\n        const resolved = resolveNode(node, filteredSnippets, config);\n        if (resolved) {\n            result.push(resolved)\n        }\n    }\n\n    return result;\n}\n\n/**\n * Converts given raw snippets into internal snippets representation\n */\nexport function convertSnippets(snippets: SnippetsMap): CSSSnippet[] {\n    const result: CSSSnippet[] = [];\n    for (const key of Object.keys(snippets)) {\n        result.push(createSnippet(key, snippets[key]));\n    }\n\n    return nest(result);\n}\n\n/**\n * Resolves given node: finds matched CSS snippets using fuzzy match and resolves\n * keyword aliases from node value\n */\nfunction resolveNode(node: CSSProperty, snippets: CSSSnippet[], config: Config): CSSProperty | null {\n    if (!resolveGradient(node, config)) {\n        const score = config.options['stylesheet.fuzzySearchMinScore'];\n        if (isValueScope(config)) {\n            // Resolve as value of given CSS property\n            const propName = config.context!.name;\n            const snippet = snippets.find(s => s.type === CSSSnippetType.Property && s.property === propName) as CSSSnippetProperty | undefined;\n            resolveValueKeywords(node, config, snippet, score);\n            node.snippet = snippet;\n        } else if (node.name) {\n            const snippet = findBestMatch(node.name, snippets, score, true);\n            node.snippet = snippet;\n\n            if (snippet) {\n                const resolved = snippet.type === CSSSnippetType.Property\n                    ? resolveAsProperty(node, snippet, config)\n                    : resolveAsSnippet(node, snippet)\n                if (resolved) {\n                    node = resolved\n                } else if (config.options['stylesheet.strictMatch']) {\n                    return null\n                }\n\n            }\n        }\n    }\n\n    if (node.name || config.context) {\n        // Resolve numeric values for CSS properties only\n        resolveNumericValue(node, config);\n    }\n\n    return node;\n}\n\n/**\n * Resolves CSS gradient shortcut from given property, if possible\n */\nfunction resolveGradient(node: CSSProperty, config: Config): boolean {\n    let gradientFn: FunctionCall | null = null;\n    const cssVal = node.value.length === 1 ? node.value[0]! : null;\n\n    if (cssVal && cssVal.value.length === 1) {\n        const v = cssVal.value[0]!;\n        if (v.type === 'FunctionCall' && v.name === gradientName) {\n            gradientFn = v;\n        }\n    }\n\n    if (gradientFn || node.name === gradientName) {\n        if (!gradientFn) {\n            gradientFn = {\n                type: 'FunctionCall',\n                name: 'linear-gradient',\n                arguments: [cssValue(field(0, ''))]\n            };\n        } else {\n            gradientFn = {\n                ...gradientFn,\n                name: 'linear-gradient'\n            };\n        }\n\n        if (!config.context) {\n            node.name = 'background-image';\n        }\n        node.value = [cssValue(gradientFn)];\n        return true;\n    }\n\n    return false;\n}\n\n/**\n * Resolves given parsed abbreviation node as CSS property\n */\nfunction resolveAsProperty(node: CSSProperty, snippet: CSSSnippetProperty, config: Config): CSSProperty | null {\n    const abbr = node.name!;\n\n    // Check for unmatched part of abbreviation\n    // For example, in `dib` abbreviation the matched part is `d` and `ib` should\n    // be considered as inline value. If unmatched fragment exists, we should check\n    // if it matches actual value of snippet. If either explicit value is specified\n    // or unmatched fragment did not resolve to to a keyword, we should consider\n    // matched snippet as invalid\n    const inlineValue = getUnmatchedPart(abbr, snippet.key);\n    if (inlineValue) {\n        if (node.value.length) {\n            // Already have value: unmatched part indicates matched snippet is invalid\n            return null;\n        }\n        const kw = resolveKeyword(inlineValue, config, snippet);\n        if (!kw) {\n            return null;\n        }\n        node.value.push(cssValue(kw));\n    }\n\n    node.name = snippet.property;\n\n    if (node.value.length) {\n        // Replace keyword alias from current abbreviation node with matched keyword\n        resolveValueKeywords(node, config, snippet);\n    } else if (snippet.value.length) {\n        const defaultValue = snippet.value[0]!;\n\n        // https://github.com/emmetio/emmet/issues/558\n        // We should auto-select inserted value only if there’s multiple value\n        // choice\n        node.value = snippet.value.length === 1 || defaultValue.some(hasField)\n            ? defaultValue\n            : defaultValue.map(n => wrapWithField(n, config));\n    }\n\n    return node;\n}\n\nfunction resolveValueKeywords(node: CSSProperty, config: Config, snippet?: CSSSnippetProperty, minScore?: number) {\n    for (const cssVal of node.value) {\n        const value: Value[] = [];\n        for (const token of cssVal.value) {\n            if (token.type === 'Literal') {\n                value.push(resolveKeyword(token.value, config, snippet, minScore) || token);\n            } else if (token.type === 'FunctionCall') {\n                // For function calls, we should find matching function call\n                // and merge arguments\n                const match = resolveKeyword(token.name, config, snippet, minScore);\n                if (match && match.type === 'FunctionCall') {\n                    value.push({\n                        ...match,\n                        arguments: token.arguments.concat(match.arguments.slice(token.arguments.length))\n                    });\n                } else {\n                    value.push(token);\n                }\n            } else {\n                value.push(token);\n            }\n        }\n        cssVal.value = value;\n    }\n}\n\n/**\n * Resolves given parsed abbreviation node as a snippet: a plain code chunk\n */\nfunction resolveAsSnippet(node: CSSProperty, snippet: CSSSnippetRaw): CSSProperty {\n    // When resolving snippets, we have to do the following:\n    // 1. Replace field placeholders with actual field tokens.\n    // 2. If input values given, put them instead of fields\n    let offset = 0;\n    let m: RegExpExecArray | null;\n    const reField = /\\$\\{(\\d+)(:[^}]+)?\\}/g;\n    const inputValue = node.value[0];\n    const outputValue: Value[] = [];\n\n    while (m = reField.exec(snippet.value)) {\n        if (offset !== m.index) {\n            outputValue.push(literal(snippet.value.slice(offset, m.index)));\n        }\n        offset = m.index + m[0].length;\n        if (inputValue && inputValue.value.length) {\n            outputValue.push(inputValue.value.shift()!);\n        } else {\n            outputValue.push(field(Number(m[1]), m[2] ? m[2].slice(1) : ''));\n        }\n    }\n\n    const tail = snippet.value.slice(offset);\n    if (tail) {\n        outputValue.push(literal(tail));\n    }\n\n    node.name = void 0;\n    node.value = [cssValue(...outputValue)];\n    return node;\n}\n\n/**\n * Finds best matching item from `items` array\n * @param abbr  Abbreviation to match\n * @param items List of items for match\n * @param minScore The minimum score the best matched item should have to be a valid match.\n */\nexport function findBestMatch<T extends MatchInput>(abbr: string, items: T[], minScore = 0, partialMatch = false): T | null {\n    let matchedItem: T | null = null;\n    let maxScore = 0;\n\n    for (const item of items) {\n        const score = calculateScore(abbr, getScoringPart(item), partialMatch);\n\n        if (score === 1) {\n            // direct hit, no need to look further\n            return item;\n        }\n\n        if (score && score >= maxScore) {\n            maxScore = score;\n            matchedItem = item;\n        }\n    }\n\n    return maxScore >= minScore ? matchedItem : null;\n}\n\nfunction getScoringPart(item: MatchInput): string {\n    return typeof item === 'string' ? item : item.key;\n}\n\n/**\n * Returns a part of `abbr` that wasn’t directly matched against `str`.\n * For example, if abbreviation `poas` is matched against `position`,\n * the unmatched part will be `as` since `a` wasn’t found in string stream\n */\nfunction getUnmatchedPart(abbr: string, str: string): string {\n    for (let i = 0, lastPos = 0; i < abbr.length; i++) {\n        lastPos = str.indexOf(abbr[i], lastPos);\n        if (lastPos === -1) {\n            return abbr.slice(i);\n        }\n        lastPos++;\n    }\n\n    return '';\n}\n\n/**\n * Resolves given keyword shorthand into matched snippet keyword or global keyword,\n * if possible\n */\nfunction resolveKeyword(kw: string, config: Config, snippet?: CSSSnippetProperty, minScore?: number): Literal | FunctionCall | null {\n    let ref: string | null;\n\n    if (snippet) {\n        if (ref = findBestMatch(kw, Object.keys(snippet.keywords), minScore)) {\n            return snippet.keywords[ref];\n        }\n\n        for (const dep of snippet.dependencies) {\n            if (ref = findBestMatch(kw, Object.keys(dep.keywords), minScore)) {\n                return dep.keywords[ref];\n            }\n        }\n    }\n\n    if (ref = findBestMatch(kw, config.options['stylesheet.keywords'], minScore)) {\n        return literal(ref);\n    }\n\n    return null;\n}\n\n/**\n * Resolves numeric values in given abbreviation node\n */\nfunction resolveNumericValue(node: CSSProperty, config: Config) {\n    const aliases = config.options['stylesheet.unitAliases'];\n    const unitless = config.options['stylesheet.unitless'];\n\n    for (const v of node.value) {\n        for (const t of v.value) {\n            if (t.type === 'NumberValue') {\n                if (t.unit) {\n                    t.unit = aliases[t.unit] || t.unit;\n                } else if (t.value !== 0 && !unitless.includes(node.name!)) {\n                    t.unit = t.rawValue.includes('.')\n                        ? config.options['stylesheet.floatUnit']\n                        : config.options['stylesheet.intUnit'];\n                }\n            }\n        }\n    }\n}\n\n/**\n * Constructs CSS value token\n */\nfunction cssValue(...args: Value[]): CSSValue {\n    return {\n        type: 'CSSValue',\n        value: args\n    };\n}\n\n/**\n * Constructs literal token\n */\nfunction literal(value: string): Literal {\n    return { type: 'Literal', value };\n}\n\n/**\n * Constructs field token\n */\nfunction field(index: number, name: string): Field {\n    return { type: 'Field', index, name };\n}\n\n/**\n * Check if given value contains fields\n */\nfunction hasField(value: CSSValue): boolean {\n    for (const v of value.value) {\n        if (v.type === 'Field' || (v.type === 'FunctionCall' && v.arguments.some(hasField))) {\n            return true;\n        }\n    }\n\n    return false;\n}\n\ninterface WrapState {\n    index: number;\n}\n\n/**\n * Wraps tokens of given abbreviation with fields\n */\nfunction wrapWithField(node: CSSValue, config: Config, state: WrapState = { index: 1 }): CSSValue {\n    let value: Value[] = [];\n    for (const v of node.value) {\n        switch (v.type) {\n            case 'ColorValue':\n                value.push(field(state.index++, color(v, config.options['stylesheet.shortHex'])));\n                break;\n            case 'Literal':\n                value.push(field(state.index++, v.value));\n                break;\n            case 'NumberValue':\n                value.push(field(state.index++, `${v.value}${v.unit}`));\n                break;\n            case 'StringValue':\n                const q = v.quote === 'single' ? '\\'' : '\"';\n                value.push(field(state.index++, q + v.value + q));\n                break;\n            case 'FunctionCall':\n                value.push(field(state.index++, v.name), literal('('));\n                for (let i = 0, il = v.arguments.length; i < il; i++) {\n                    value = value.concat(wrapWithField(v.arguments[i], config, state).value);\n                    if (i !== il - 1) {\n                        value.push(literal(', '));\n                    }\n                }\n                value.push(literal(')'));\n                break;\n            default:\n                value.push(v);\n        }\n    }\n\n    return {...node, value };\n}\n\n/**\n * Check if abbreviation should be expanded in CSS value context\n */\nfunction isValueScope(config: Config): boolean {\n    if (config.context) {\n        return config.context.name === CSSAbbreviationScope.Value || !config.context.name.startsWith('@@');\n    }\n\n    return false;\n}\n\n/**\n * Returns snippets for given scope\n */\nfunction getSnippetsForScope(snippets: CSSSnippet[], config: Config): CSSSnippet[] {\n    if (config.context) {\n        if (config.context.name === CSSAbbreviationScope.Section) {\n            return snippets.filter(s => s.type === CSSSnippetType.Raw);\n        }\n\n        if (config.context.name === CSSAbbreviationScope.Property) {\n            return snippets.filter(s => s.type === CSSSnippetType.Property);\n        }\n    }\n\n    return snippets;\n}\n"
  },
  {
    "path": "src/stylesheet/score.ts",
    "content": "/**\n * Calculates how close `str1` matches `str2` using fuzzy match.\n * How matching works:\n * – first characters of both `str1` and `str2` *must* match\n * – `str1` length larger than `str2` length is allowed only when `unmatched` is true\n * – ideal match is when `str1` equals to `str2` (score: 1)\n * – next best match is `str2` starts with `str1` (score: 1 × percent of matched characters)\n * – other scores depend on how close characters of `str1` to the beginning of `str2`\n * @param partialMatch Allow length `str1` to be greater than `str2` length\n */\nexport default function scoreMatch(str1: string, str2: string, partialMatch = false) {\n    str1 = str1.toLowerCase();\n    str2 = str2.toLowerCase();\n\n    if (str1 === str2) {\n        return 1;\n    }\n\n    // Both strings MUST start with the same character\n    if (!str1 || !str2 || str1.charCodeAt(0) !== str2.charCodeAt(0)) {\n        return 0;\n    }\n\n    const str1Len = str1.length;\n    const str2Len = str2.length;\n\n    if (!partialMatch && str1Len > str2Len) {\n        return 0;\n    }\n\n    // Characters from `str1` which are closer to the beginning of a `str2` should\n    // have higher score.\n    // For example, if `str2` is `abcde`, it’s max score is:\n    // 5 + 4 + 3 + 2 + 1 = 15 (sum of character positions in reverse order)\n    // Matching `abd` against `abcde` should produce:\n    // 5 + 4 + 2 = 11\n    // Acronym bonus for match right after `-`. Matching `abd` against `abc-de`\n    // should produce:\n    // 6 + 5 + 4 (use `d` position in `abd`, not in abc-de`)\n\n    const minLength = Math.min(str1Len, str2Len);\n    const maxLength = Math.max(str1Len, str2Len);\n    let i = 1;\n    let j = 1;\n    let score = maxLength;\n    let ch1 = 0;\n    let ch2 = 0;\n    let found = false;\n    let acronym = false;\n\n    while (i < str1Len) {\n        ch1 = str1.charCodeAt(i);\n        found = false;\n        acronym = false;\n\n        while (j < str2Len) {\n            ch2 = str2.charCodeAt(j);\n\n            if (ch1 === ch2) {\n                found = true;\n                score += maxLength - (acronym ? i : j);\n                break;\n            }\n\n            // add acronym bonus for exactly next match after unmatched `-`\n            acronym = ch2 === 45 /* - */;\n            j++;\n        }\n\n        if (!found) {\n            if (!partialMatch) {\n                return 0;\n            }\n            break;\n        }\n\n        i++;\n    }\n\n    const matchRatio = i / maxLength;\n    const delta = maxLength - minLength;\n    const maxScore = sum(maxLength) - sum(delta);\n    return (score * matchRatio) / maxScore;\n}\n\n/**\n * Calculates sum of first `n` numbers, e.g. 1+2+3+...n\n */\nfunction sum(n: number): number {\n    return n * (n + 1) / 2;\n}\n"
  },
  {
    "path": "src/stylesheet/snippets.ts",
    "content": "import parse from '@emmetio/css-abbreviation';\nimport type { CSSValue, ParseOptions, FunctionCall, Literal } from '@emmetio/css-abbreviation';\n\nexport type CSSSnippet = CSSSnippetRaw | CSSSnippetProperty;\n\ninterface KeywordMap {\n    [name: string]: FunctionCall | Literal;\n}\n\nexport const enum CSSSnippetType {\n    Raw = 'Raw',\n    Property = 'Property'\n}\n\ninterface CSSSnippetBase {\n    type: CSSSnippetType;\n    key: string;\n}\n\nexport interface CSSSnippetRaw extends CSSSnippetBase {\n    type: CSSSnippetType.Raw;\n    value: string;\n}\n\nexport interface CSSSnippetProperty extends CSSSnippetBase {\n    type: CSSSnippetType.Property;\n    property: string;\n    value: CSSValue[][];\n    keywords: KeywordMap;\n    dependencies: CSSSnippetProperty[];\n}\n\nconst reProperty = /^([a-z-]+)(?:\\s*:\\s*([^\\n\\r;]+?);*)?$/;\nconst opt: ParseOptions = { value: true };\n\n/**\n * Creates structure for holding resolved CSS snippet\n */\nexport default function createSnippet(key: string, value: string): CSSSnippet {\n    // A snippet could be a raw text snippet (e.g. arbitrary text string) or a\n    // CSS property with possible values separated by `|`.\n    // In latter case, we have to parse snippet as CSS abbreviation\n    const m = value.match(reProperty);\n    if (m) {\n        const keywords: KeywordMap = {};\n        const parsed: CSSValue[][] = m[2] ? m[2].split('|').map(parseValue) : [];\n\n        for (const item of parsed) {\n            for (const cssVal of item) {\n                collectKeywords(cssVal, keywords);\n            }\n        }\n\n        return {\n            type: CSSSnippetType.Property,\n            key,\n            property: m[1],\n            value: parsed,\n            keywords,\n            dependencies: []\n        };\n    }\n\n    return { type: CSSSnippetType.Raw, key, value };\n}\n\n/**\n * Nests more specific CSS properties into shorthand ones, e.g.\n * `background-position-x` -> `background-position` -> `background`\n */\nexport function nest(snippets: CSSSnippet[]): CSSSnippet[] {\n    snippets = snippets.slice().sort(snippetsSort);\n    const stack: CSSSnippetProperty[] = [];\n    let prev: CSSSnippet;\n\n    // For sorted list of CSS properties, create dependency graph where each\n    // shorthand property contains its more specific one, e.g.\n    // background -> background-position -> background-position-x\n    for (const cur of snippets.filter(isProperty)) {\n        // Check if current property belongs to one from parent stack.\n        // Since `snippets` array is sorted, items are perfectly aligned\n        // from shorthands to more specific variants\n        while (stack.length) {\n            prev = stack[stack.length - 1];\n\n            if (cur.property.startsWith(prev.property!)\n                && cur.property.charCodeAt(prev.property!.length) === 45 /* - */) {\n                prev.dependencies.push(cur);\n                stack.push(cur);\n                break;\n            }\n\n            stack.pop();\n        }\n\n        if (!stack.length) {\n            stack.push(cur);\n        }\n\n    }\n\n    return snippets;\n}\n\n/**\n * A sorting function for array of snippets\n */\nfunction snippetsSort(a: CSSSnippet, b: CSSSnippet): number {\n    if (a.key === b.key) {\n        return 0;\n    }\n\n    return a.key < b.key ? -1 : 1;\n}\n\nfunction parseValue(value: string): CSSValue[] {\n    return parse(value.trim(), opt)[0].value;\n}\n\nfunction isProperty(snippet: CSSSnippet): snippet is CSSSnippetProperty {\n    return snippet.type === CSSSnippetType.Property;\n}\n\nfunction collectKeywords(cssVal: CSSValue, dest: KeywordMap) {\n    for (const v of cssVal.value) {\n        if (v.type === 'Literal') {\n            dest[v.value] = v;\n        } else if (v.type === 'FunctionCall') {\n            dest[v.name] = v;\n        } else if (v.type === 'Field') {\n            // Create literal from field, if available\n            const value = v.name.trim();\n            if (value) {\n                dest[value] = { type: 'Literal', value };\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "test/assets/stringify.ts",
    "content": "import { Abbreviation, AbbreviationNode, Value, AbbreviationAttribute } from '@emmetio/abbreviation';\n\nexport default function stringify(abbr: Abbreviation): string {\n    return abbr.children.map(elem).join('');\n}\n\nfunction elem(node: AbbreviationNode): string {\n    const name = node.name || '?';\n    const attributes = node.attributes\n        ? node.attributes.map(attr => ' ' + attribute(attr)).join('')\n        : '';\n    const value = node.value ? stringifyValue(node.value) : '';\n    const repeat = node.repeat ? `*${node.repeat.count}@${node.repeat.value}` : '';\n\n    if (!node.name && !node.attributes) {\n        return value;\n    }\n\n    return node.selfClosing && !node.value && !node.children.length\n        ? `<${name}${repeat}${attributes} />`\n        : `<${name}${repeat}${attributes}>${value}${node.children.map(elem).join('')}</${name}>`;\n\n}\n\nfunction attribute(attr: AbbreviationAttribute): string {\n    const name = attr.name || '?';\n    let before = '\"';\n    let after = '\"';\n    if (attr.valueType === 'expression') {\n        before = '{';\n        after = '}';\n    }\n\n    const value = before + (attr.value ? `${stringifyValue(attr.value)}` : '') + after;\n    return `${name}=${value}`;\n}\n\nfunction stringifyValue(items: Value[]): string {\n    return items.map(item =>\n        typeof item === 'string'\n            ? item\n            : (item.name ? `\\${${item.index!}:${item.name}}` : `\\${${item.index!}}`)).join('');\n}\n"
  },
  {
    "path": "test/expand.ts",
    "content": "import { describe, it } from 'node:test';\nimport { strictEqual as equal } from 'node:assert';\nimport expand, { resolveConfig } from '../src';\n\ndescribe('Expand Abbreviation', () => {\n    describe('Markup', () => {\n        it('basic', () => {\n            equal(expand('input[value=\"text$\"]*2'), '<input type=\"text\" value=\"text1\"><input type=\"text\" value=\"text2\">');\n            equal(expand('ul>.item$*2'), '<ul>\\n\\t<li class=\"item1\"></li>\\n\\t<li class=\"item2\"></li>\\n</ul>');\n\n            // insert text into abbreviation\n            equal(expand('ul>.item$*', { text: ['foo', 'bar'] }), '<ul>\\n\\t<li class=\"item1\">foo</li>\\n\\t<li class=\"item2\">bar</li>\\n</ul>');\n\n            // insert TextMate-style fields/tabstops in output\n            equal(expand('ul>.item$*2', {\n                options: {\n                    'output.field': (index, placeholder) => `\\${${index}${placeholder ? ':' + placeholder : ''}}`\n                }\n            }), '<ul>\\n\\t<li class=\"item1\">${1}</li>\\n\\t<li class=\"item2\">${2}</li>\\n</ul>');\n\n            // https://github.com/emmetio/emmet/issues/725\n            equal(expand('textarea'), '<textarea name=\"\" id=\"\"></textarea>');\n        });\n\n        it('attributes', () => {\n            const snippets = {\n                test: 'test[!foo bar. baz={}]'\n            };\n            const opt = { snippets };\n            const reverse = {\n                options: { 'output.reverseAttributes': true },\n                snippets\n            };\n\n            equal(expand('a.test'), '<a href=\"\" class=\"test\"></a>');\n            equal(expand('a.test', reverse), '<a class=\"test\" href=\"\"></a>');\n\n            equal(expand('test', opt), '<test bar=\"bar\" baz={}></test>');\n            equal(expand('test[foo]', opt), '<test bar=\"bar\" baz={}></test>');\n            equal(expand('test[baz=a foo=1]', opt), '<test foo=\"1\" bar=\"bar\" baz={a}></test>');\n\n            equal(expand('map'), '<map name=\"\"></map>');\n            equal(expand('map[]'), '<map name=\"\"></map>');\n            equal(expand('map[name=\"valid\"]'), '<map name=\"valid\"></map>');\n            equal(expand('map[href=\"invalid\"]'), '<map name=\"\" href=\"invalid\"></map>');\n\n            equal(expand('data'), '<data value=\"\"></data>');\n            equal(expand('data[value=5]'), '<data value=\"5\"></data>');\n            equal(expand('meter'), '<meter value=\"\"></meter>');\n            equal(expand('meter[min=4 max=6]'), '<meter value=\"\" min=\"4\" max=\"6\"></meter>');\n            equal(expand('time'), '<time datetime=\"\"></time>');\n            equal(expand('time[datetime=2023-07-01]'), '<time datetime=\"2023-07-01\"></time>');\n\n            // Apply attributes in reverse order\n            equal(expand('test', reverse), '<test bar=\"bar\" baz={}></test>');\n            equal(expand('test[foo]', reverse), '<test bar=\"bar\" baz={}></test>');\n            equal(expand('test[baz=a foo=1]', reverse), '<test baz={a} foo=\"1\" bar=\"bar\"></test>');\n        });\n\n        it('expressions', () => {\n            equal(expand('span{{foo}}'), '<span>{foo}</span>');\n            equal(expand('span{foo}'), '<span>foo</span>');\n            equal(expand('span[foo={bar}]'), '<span foo={bar}></span>');\n            equal(expand('span[foo={{bar}}]'), '<span foo={{bar}}></span>');\n        });\n\n        it('numbering', () => {\n            equal(expand('ul>li.item$@-*5'), '<ul>\\n\\t<li class=\"item5\"></li>\\n\\t<li class=\"item4\"></li>\\n\\t<li class=\"item3\"></li>\\n\\t<li class=\"item2\"></li>\\n\\t<li class=\"item1\"></li>\\n</ul>');\n        });\n\n        it('syntax', () => {\n            equal(expand('ul>.item$*2', { syntax: 'html' }), '<ul>\\n\\t<li class=\"item1\"></li>\\n\\t<li class=\"item2\"></li>\\n</ul>');\n            equal(expand('ul>.item$*2', { syntax: 'slim' }), 'ul\\n\\tli.item1 \\n\\tli.item2 ');\n            equal(expand('xsl:variable[name=a select=b]>div', { syntax: 'xsl' }), '<xsl:variable name=\"a\">\\n\\t<div></div>\\n</xsl:variable>');\n        });\n\n        it('custom profile', () => {\n            equal(expand('img'), '<img src=\"\" alt=\"\">');\n            equal(expand('img', { options: { 'output.selfClosingStyle': 'xhtml' } }), '<img src=\"\" alt=\"\" />');\n        });\n\n        it('custom variables', () => {\n            const variables = { charset: 'ru-RU' };\n\n            equal(expand('[charset=${charset}]{${charset}}'), '<div charset=\"utf-8\">utf-8</div>');\n            equal(expand('[charset=${charset}]{${charset}}', { variables }), '<div charset=\"ru-RU\">ru-RU</div>');\n        });\n\n        it('custom snippets', () => {\n            const snippets = {\n                link: 'link[foo=bar href]/',\n                foo: '.foo[bar=baz]',\n                repeat: 'div>ul>li{Hello World}*3'\n            };\n\n            equal(expand('foo', { snippets }), '<div class=\"foo\" bar=\"baz\"></div>');\n\n            // `link:css` depends on `link` snippet so changing it will result in\n            // altered `link:css` result\n            equal(expand('link:css'), '<link rel=\"stylesheet\" href=\"style.css\">');\n            equal(expand('link:css', { snippets }), '<link foo=\"bar\" href=\"style.css\">');\n\n            // https://github.com/emmetio/emmet/issues/468\n            equal(expand('repeat', { snippets }), '<div>\\n\\t<ul>\\n\\t\\t<li>Hello World</li>\\n\\t\\t<li>Hello World</li>\\n\\t\\t<li>Hello World</li>\\n\\t</ul>\\n</div>');\n\n            // https://github.com/emmetio/emmet/issues/725\n            equal(expand('tarea'), '<textarea name=\"\" id=\"\"></textarea>');\n            equal(expand('tarea:c'), '<textarea name=\"\" id=\"\" cols=\"30\"></textarea>')\n            equal(expand('tarea:r'), '<textarea name=\"\" id=\"\" rows=\"10\"></textarea>')\n            equal(expand('tarea:cr'), '<textarea name=\"\" id=\"\" cols=\"30\" rows=\"10\"></textarea>')\n        });\n\n        it('formatter options', () => {\n            equal(expand('ul>.item$*2'), '<ul>\\n\\t<li class=\"item1\"></li>\\n\\t<li class=\"item2\"></li>\\n</ul>');\n            equal(expand('ul>.item$*2', { options: { 'comment.enabled': true } }),\n                '<ul>\\n\\t<li class=\"item1\"></li>\\n\\t<!-- /.item1 -->\\n\\t<li class=\"item2\"></li>\\n\\t<!-- /.item2 -->\\n</ul>');\n\n            equal(expand('div>p'), '<div>\\n\\t<p></p>\\n</div>');\n            equal(expand('div>p', { options: { 'output.formatLeafNode': true } }), '<div>\\n\\t<p>\\n\\t\\t\\n\\t</p>\\n</div>');\n        });\n\n        it('JSX', () => {\n            const config = { syntax: 'jsx' };\n            equal(expand('div#foo.bar', config), '<div id=\"foo\" className=\"bar\"></div>');\n            equal(expand('label[for=a]', config), '<label htmlFor=\"a\"></label>');\n            equal(expand('Foo.Bar', config), '<Foo.Bar></Foo.Bar>');\n            equal(expand('div.{theme.style}', config), '<div className={theme.style}></div>');\n        });\n\n        it('override attributes', () => {\n            const config = { syntax: 'jsx' };\n            equal(expand('.bar', config), '<div className=\"bar\"></div>');\n            equal(expand('..bar', config), '<div styleName={styles.bar}></div>');\n            equal(expand('..foo-bar', config), '<div styleName={styles[\\'foo-bar\\']}></div>');\n\n            equal(expand('.foo', { syntax: 'vue' }), '<div class=\"foo\"></div>');\n            equal(expand('..foo', { syntax: 'vue' }), '<div :class=\"foo\"></div>');\n        });\n\n        it('overrides attributes with custom config', () => {\n            const attrConfig = {\n                syntax: 'jsx',\n                options: {\n                    'markup.attributes': {\n                        'class': 'className',\n                        'class*': 'classStarName',\n                    }\n                }\n            };\n            equal(expand('.foo', attrConfig), '<div className=\"foo\"></div>');\n            equal(expand('..foo', attrConfig), '<div classStarName={styles.foo}></div>');\n            const prefixConfig = {\n                syntax: 'jsx',\n                options: {\n                    'markup.valuePrefix': {\n                        'class*': 'class'\n                    }\n                }\n            };\n            equal(expand('..foo', prefixConfig), '<div styleName={class.foo}></div>');\n        });\n\n        it('wrap with abbreviation', () => {\n            equal(expand('div>ul', { text: ['<div>line1</div>\\n<div>line2</div>'] }),\n                '<div>\\n\\t<ul>\\n\\t\\t<div>line1</div>\\n\\t\\t<div>line2</div>\\n\\t</ul>\\n</div>');\n            equal(expand('p', { text: 'foo\\nbar' }), '<p>\\n\\tfoo\\n\\tbar\\n</p>');\n            equal(expand('p', { text: '<div>foo</div>' }), '<p>\\n\\t<div>foo</div>\\n</p>');\n            equal(expand('p', { text: '<span>foo</span>' }), '<p><span>foo</span></p>');\n            equal(expand('p', { text: 'foo<span>foo</span>' }), '<p>foo<span>foo</span></p>');\n            equal(expand('p', { text: 'foo<div>foo</div>' }), '<p>foo<div>foo</div></p>');\n        });\n\n        it('wrap with abbreviation href', () => {\n            equal(expand('a', { text: ['www.google.it'] }), '<a href=\"http://www.google.it\">www.google.it</a>');\n            equal(expand('a', { text: ['then www.google.it'] }), '<a href=\"\">then www.google.it</a>');\n            equal(expand('a', { text: ['www.google.it'], options: { 'markup.href': false } }), '<a href=\"\">www.google.it</a>');\n\n            equal(expand('map[name=\"https://example.com\"]', { text: ['some text'] }),\n                '<map name=\"https://example.com\">some text</map>');\n            equal(expand('map[href=\"https://example.com\"]', { text: ['some text'] }),\n                '<map name=\"\" href=\"https://example.com\">some text</map>');\n            equal(expand('map[name=\"https://example.com\"]>b', { text: ['some text'] }),\n                '<map name=\"https://example.com\"><b>some text</b></map>');\n\n            equal(expand('a[href=\"https://example.com\"]>b', { text: ['<u>some text false</u>'], options: { 'markup.href': false } }),\n                '<a href=\"https://example.com\"><b><u>some text false</u></b></a>');\n            equal(expand('a[href=\"https://example.com\"]>b', { text: ['<u>some text true</u>'], options: { 'markup.href': true } }),\n                '<a href=\"https://example.com\"><b><u>some text true</u></b></a>');\n            equal(expand('a[href=\"https://example.com\"]>div', { text: ['<p>some text false</p>'], options: { 'markup.href': false } }),\n                '<a href=\"https://example.com\">\\n\\t<div>\\n\\t\\t<p>some text false</p>\\n\\t</div>\\n</a>');\n            equal(expand('a[href=\"https://example.com\"]>div', { text: ['<p>some text true</p>'], options: { 'markup.href': true } }),\n                '<a href=\"https://example.com\">\\n\\t<div>\\n\\t\\t<p>some text true</p>\\n\\t</div>\\n</a>');\n        });\n\n        it('class names', () => {\n            equal(expand('div.foo/'), '<div class=\"foo\">');\n            equal(expand('div.foo1/2'), '<div class=\"foo1/2\"></div>');\n            equal(expand('div.foo.1/2'), '<div class=\"foo 1/2\"></div>');\n        })\n\n        // it.only('debug', () => {\n        //     equal(expand('link:css'), '<link rel=\"stylesheet\" href=\"style.css\">');\n        // });\n    });\n\n    describe('Pug templates', () => {\n        const config = resolveConfig({ syntax: 'pug' });\n        it('basic', () => {\n            equal(expand('!', config), 'doctype html\\nhtml(lang=\"en\")\\n\\thead\\n\\t\\tmeta(charset=\"utf-8\")\\n\\t\\tmeta(name=\"viewport\", content=\"width=device-width, initial-scale=1.0\")\\n\\t\\ttitle Document\\n\\tbody ');\n        });\n    });\n});\n"
  },
  {
    "path": "test/extract-abbreviation.ts",
    "content": "import { describe, it } from 'node:test';\nimport { deepStrictEqual, strictEqual, ok } from 'node:assert';\nimport extractAbbreviation, { type ExtractOptions, type ExtractedAbbreviation } from '../src/extract-abbreviation';\nimport isAtHTMLTag from '../src/extract-abbreviation/is-html';\nimport scanner from '../src/extract-abbreviation/reader';\nimport { consumeQuoted } from '../src/extract-abbreviation/quotes';\n\nfunction extract(abbr: string, options?: Partial<ExtractOptions>) {\n    let caretPos: number | undefined = abbr.indexOf('|');\n    if (caretPos !== -1) {\n        abbr = abbr.slice(0, caretPos) + abbr.slice(caretPos + 1);\n    } else {\n        caretPos = void 0;\n    }\n\n    return extractAbbreviation(abbr, caretPos, options);\n}\n\nfunction result(abbreviation: string, location: number, start = location): ExtractedAbbreviation {\n    return {\n        abbreviation,\n        location,\n        start: start != null ? start : location,\n        end: location + abbreviation.length\n    };\n}\n\ndescribe('Extract abbreviation', () => {\n    it('basic', () => {\n        deepStrictEqual(extract('.bar'), result('.bar', 0));\n        deepStrictEqual(extract('.foo .bar'), result('.bar', 5));\n        deepStrictEqual(extract('.foo @bar'), result('@bar', 5));\n        deepStrictEqual(extract('.foo img/'), result('img/', 5));\n        deepStrictEqual(extract('текстdiv'), result('div', 5));\n        deepStrictEqual(extract('foo div[foo=\"текст\" bar=текст2]'), result('div[foo=\"текст\" bar=текст2]', 4));\n\n        // https://github.com/emmetio/emmet/issues/577\n        deepStrictEqual(\n            extract('table>(tr.prefix-intro>td*1)+(tr.prefix-pro-con>th*1+td*3)+(tr.prefix-key-specs>th[colspan=2]*1+td[colspan=2]*3)+(tr.prefix-key-find-online>th[colspan=2]*1+td*2)'),\n            result('table>(tr.prefix-intro>td*1)+(tr.prefix-pro-con>th*1+td*3)+(tr.prefix-key-specs>th[colspan=2]*1+td[colspan=2]*3)+(tr.prefix-key-find-online>th[colspan=2]*1+td*2)', 0));\n    });\n\n    it('abbreviation with operators', () => {\n        deepStrictEqual(extract('a foo+bar.baz'), result('foo+bar.baz', 2));\n        deepStrictEqual(extract('a foo>bar+baz*3'), result('foo>bar+baz*3', 2));\n    });\n\n    it('abbreviation with attributes', () => {\n        deepStrictEqual(extract('a foo[bar|]'), result('foo[bar]', 2));\n        deepStrictEqual(extract('a foo[bar=\"baz\" a b]'), result('foo[bar=\"baz\" a b]', 2));\n        deepStrictEqual(extract('foo bar[a|] baz'), result('bar[a]', 4));\n    });\n\n    it('tag test', () => {\n        deepStrictEqual(extract('<foo>bar[a b=\"c\"]>baz'), result('bar[a b=\"c\"]>baz', 5));\n        deepStrictEqual(extract('foo>bar'), result('foo>bar', 0));\n        deepStrictEqual(extract('<foo>bar'), result('bar', 5));\n        deepStrictEqual(extract('<foo>bar[a=\"d\" b=\"c\"]>baz'), result('bar[a=\"d\" b=\"c\"]>baz', 5));\n    });\n\n    it('stylesheet abbreviation', () => {\n        deepStrictEqual(extract('foo{bar|}'), result('foo{bar}', 0));\n        deepStrictEqual(extract('foo{bar|}', { type: 'stylesheet' }), result('bar', 4));\n    });\n\n    it('prefixed extract', () => {\n        deepStrictEqual(extract('<foo>bar[a b=\"c\"]>baz'), result('bar[a b=\"c\"]>baz', 5));\n        deepStrictEqual(extract('<foo>bar[a b=\"c\"]>baz', { prefix: '<' }), result('foo>bar[a b=\"c\"]>baz', 1, 0));\n        deepStrictEqual(extract('<foo>bar[a b=\"<\"]>baz', { prefix: '<' }), result('foo>bar[a b=\"<\"]>baz', 1, 0));\n        deepStrictEqual(extract('<foo>bar{<}>baz', { prefix: '<' }), result('foo>bar{<}>baz', 1, 0));\n\n        // Multiple prefix characters\n        deepStrictEqual(extract('foo>>>bar[a b=\"c\"]>baz', { prefix: '>>>' }), result('bar[a b=\"c\"]>baz', 6, 3));\n\n        // Absent prefix\n        strictEqual(extract('<foo>bar[a b=\"c\"]>baz', { prefix: '&&' }), void 0);\n    });\n\n    it('brackets inside curly braces', () => {\n        deepStrictEqual(extract('foo div{[}+a{}'), result('div{[}+a{}', 4));\n        deepStrictEqual(extract('div{}}'), undefined);\n        deepStrictEqual(extract('div{{}'), result('{}', 4));\n    });\n\n    it('HTML test', () => {\n        const html = (str: string) => isAtHTMLTag(scanner(str));\n\n        // simple tag\n        ok(html('<div>'));\n        ok(html('<div/>'));\n        ok(html('<div />'));\n        ok(html('</div>'));\n\n        // tag with attributes\n        ok(html('<div foo=\"bar\">'));\n        ok(html('<div foo=bar>'));\n        ok(html('<div foo>'));\n        ok(html('<div a=\"b\" c=d>'));\n        ok(html('<div a=b c=d>'));\n        ok(html('<div a=^b$ c=d>'));\n        ok(html('<div a=b c=^%d]$>'));\n        ok(html('<div title=привет>'));\n        ok(html('<div title=привет123>'));\n        ok(html('<foo-bar>'));\n\n        // invalid tags\n        ok(!html('div>'));\n        ok(!html('<div'));\n        ok(!html('<div привет>'));\n        ok(!html('<div =bar>'));\n        ok(!html('<div foo=>'));\n        ok(!html('[a=b c=d]>'));\n        ok(!html('div[a=b c=d]>'));\n    });\n\n    it('consume quotes', () => {\n        let s = scanner(' \"foo\"');\n        ok(consumeQuoted(s));\n        strictEqual(s.pos, 1);\n\n        s = scanner('\"foo\"');\n        ok(consumeQuoted(s));\n        strictEqual(s.pos, 0);\n\n        s = scanner('\"\"');\n        ok(consumeQuoted(s));\n        strictEqual(s.pos, 0);\n\n        s = scanner('\"a\\\\\\\"b\"');\n        ok(consumeQuoted(s));\n        strictEqual(s.pos, 0);\n\n        // don’t eat anything\n        s = scanner('foo');\n        ok(!consumeQuoted(s));\n        strictEqual(s.pos, 3);\n    });\n});\n"
  },
  {
    "path": "test/format.ts",
    "content": "import { describe, it } from 'node:test';\nimport { equal } from 'node:assert';\nimport html from '../src/markup/format/html';\nimport haml from '../src/markup/format/haml';\nimport pug from '../src/markup/format/pug';\nimport slim from '../src/markup/format/slim';\nimport parse from '../src/markup/index';\nimport createConfig, { Options } from '../src/config';\n\ndescribe('Format', () => {\n    const defaultConfig = createConfig();\n    const field = createConfig({\n        options: {\n            'output.field': (index, placeholder) => placeholder ? `\\${${index}:${placeholder}}` : `\\${${index}}`\n        }\n    });\n\n    function createProfile(options: Partial<Options>) {\n        const config = createConfig({ options });\n        return config;\n    }\n\n    describe('HTML', () => {\n        const format = (abbr: string, config = defaultConfig) => html(parse(abbr, config), config);\n        it('basic', () => {\n            equal(format('div>p'), '<div>\\n\\t<p></p>\\n</div>');\n            equal(format('div>p*3'), '<div>\\n\\t<p></p>\\n\\t<p></p>\\n\\t<p></p>\\n</div>');\n            equal(format('div#a>p.b*2>span'), '<div id=\"a\">\\n\\t<p class=\"b\"><span></span></p>\\n\\t<p class=\"b\"><span></span></p>\\n</div>');\n            equal(format('div>div>div'), '<div>\\n\\t<div>\\n\\t\\t<div></div>\\n\\t</div>\\n</div>');\n\n            equal(format('table>tr*2>td{item}*2'),\n                '<table>\\n\\t<tr>\\n\\t\\t<td>item</td>\\n\\t\\t<td>item</td>\\n\\t</tr>\\n\\t<tr>\\n\\t\\t<td>item</td>\\n\\t\\t<td>item</td>\\n\\t</tr>\\n</table>');\n        });\n\n        it('inline elements', () => {\n            const profile = createProfile({ 'output.inlineBreak': 3 });\n            const breakInline = createProfile({ 'output.inlineBreak': 1 });\n            const keepInline = createProfile({ 'output.inlineBreak': 0 });\n            const xhtml = createProfile({ 'output.selfClosingStyle': 'xhtml' });\n\n            equal(format('div>a>b*3', xhtml), '<div>\\n\\t<a href=\"\">\\n\\t\\t<b></b>\\n\\t\\t<b></b>\\n\\t\\t<b></b>\\n\\t</a>\\n</div>');\n\n            equal(format('p>i', profile), '<p><i></i></p>');\n            equal(format('p>i*2', profile), '<p><i></i><i></i></p>');\n            equal(format('p>i*2', breakInline), '<p>\\n\\t<i></i>\\n\\t<i></i>\\n</p>');\n            equal(format('p>i*3', profile), '<p>\\n\\t<i></i>\\n\\t<i></i>\\n\\t<i></i>\\n</p>');\n            equal(format('p>i*3', keepInline), '<p><i></i><i></i><i></i></p>');\n\n            equal(format('i*2', profile), '<i></i><i></i>');\n            equal(format('i*3', profile), '<i></i>\\n<i></i>\\n<i></i>');\n            equal(format('i{a}+i{b}', profile), '<i>a</i><i>b</i>');\n\n            equal(format('img[src]/+p', xhtml), '<img src=\"\" alt=\"\" />\\n<p></p>');\n            equal(format('div>img[src]/+p', xhtml), '<div>\\n\\t<img src=\"\" alt=\"\" />\\n\\t<p></p>\\n</div>');\n            equal(format('div>p+img[src]/', xhtml), '<div>\\n\\t<p></p>\\n\\t<img src=\"\" alt=\"\" />\\n</div>');\n            equal(format('div>p+img[src]/+p', xhtml), '<div>\\n\\t<p></p>\\n\\t<img src=\"\" alt=\"\" />\\n\\t<p></p>\\n</div>');\n            equal(format('div>p+img[src]/*2+p', xhtml), '<div>\\n\\t<p></p>\\n\\t<img src=\"\" alt=\"\" /><img src=\"\" alt=\"\" />\\n\\t<p></p>\\n</div>');\n            equal(format('div>p+img[src]/*3+p', xhtml), '<div>\\n\\t<p></p>\\n\\t<img src=\"\" alt=\"\" />\\n\\t<img src=\"\" alt=\"\" />\\n\\t<img src=\"\" alt=\"\" />\\n\\t<p></p>\\n</div>');\n        });\n\n        it('generate fields', () => {\n            equal(format('a[href]', field), '<a href=\"${1}\">${2}</a>');\n            equal(format('a[href]*2', field), '<a href=\"${1}\">${2}</a><a href=\"${3}\">${4}</a>');\n            equal(format('{${0} ${1:foo} ${2:bar}}*2', field), '${1} ${2:foo} ${3:bar}\\n${4} ${5:foo} ${6:bar}');\n            equal(format('{${0} ${1:foo} ${2:bar}}*2'), ' foo bar\\n foo bar');\n            equal(format('ul>li*2', field), '<ul>\\n\\t<li>${1}</li>\\n\\t<li>${2}</li>\\n</ul>');\n            equal(format('div>img[src]/', field), '<div><img src=\"${1}\" alt=\"${2}\"></div>');\n        });\n\n        // it.only('debug', () => {\n        //     equal(format('div>{foo}+{bar}+p'), '<div>\\n\\tfoobar\\n\\t<p></p>\\n</div>');\n        // });\n\n        it('mixed content', () => {\n            equal(format('div{foo}'), '<div>foo</div>');\n            equal(format('div>{foo}'), '<div>foo</div>');\n            equal(format('div>{foo}+{bar}'), '<div>\\n\\tfoo\\n\\tbar\\n</div>');\n            equal(format('div>{foo}+{bar}+p'), '<div>\\n\\tfoo\\n\\tbar\\n\\t<p></p>\\n</div>');\n            equal(format('div>{foo}+{bar}+p+{foo}+{bar}+p'), '<div>\\n\\tfoo\\n\\tbar\\n\\t<p></p>\\n\\tfoo\\n\\tbar\\n\\t<p></p>\\n</div>');\n            equal(format('div>{foo}+p+{bar}'), '<div>\\n\\tfoo\\n\\t<p></p>\\n\\tbar\\n</div>');\n            equal(format('div>{foo}>p'), '<div>\\n\\tfoo\\n\\t<p></p>\\n</div>');\n\n            equal(format('div>{<!-- ${0} -->}'), '<div><!--  --></div>');\n            equal(format('div>{<!-- ${0} -->}+p'), '<div>\\n\\t<!--  -->\\n\\t<p></p>\\n</div>');\n            equal(format('div>p+{<!-- ${0} -->}'), '<div>\\n\\t<p></p>\\n\\t<!--  -->\\n</div>');\n            equal(format('div>{<!-- ${0} -->}>p'), '<div>\\n\\t<!-- <p></p> -->\\n</div>');\n            equal(format('div>{<!-- ${0} -->}*2>p'), '<div>\\n\\t<!-- <p></p> -->\\n\\t<!-- <p></p> -->\\n</div>');\n\n            equal(format('div>{<!-- ${0} -->}>p*2'), '<div>\\n\\t<!-- \\n\\t<p></p>\\n\\t<p></p>\\n\\t-->\\n</div>');\n            equal(format('div>{<!-- ${0} -->}*2>p*2'), '<div>\\n\\t<!-- \\n\\t<p></p>\\n\\t<p></p>\\n\\t-->\\n\\t<!-- \\n\\t<p></p>\\n\\t<p></p>\\n\\t-->\\n</div>');\n\n            equal(format('div>{<!-- ${0} -->}>b'), '<div>\\n\\t<!-- <b></b> -->\\n</div>');\n            equal(format('div>{<!-- ${0} -->}>b*2'), '<div>\\n\\t<!-- <b></b><b></b> -->\\n</div>');\n            equal(format('div>{<!-- ${0} -->}>b*3'), '<div>\\n\\t<!-- \\n\\t<b></b>\\n\\t<b></b>\\n\\t<b></b>\\n\\t-->\\n</div>');\n\n            equal(format('div>{<!-- ${0} -->}', field), '<div><!-- ${1} --></div>');\n            equal(format('div>{<!-- ${0} -->}>b', field), '<div>\\n\\t<!-- <b>${1}</b> -->\\n</div>');\n        });\n\n        it('self-closing', () => {\n            const xmlStyle = createProfile({ 'output.selfClosingStyle': 'xml' });\n            const htmlStyle = createProfile({ 'output.selfClosingStyle': 'html' });\n            const xhtmlStyle = createProfile({ 'output.selfClosingStyle': 'xhtml' });\n\n            equal(format('img[src]/', htmlStyle), '<img src=\"\" alt=\"\">');\n            equal(format('img[src]/', xhtmlStyle), '<img src=\"\" alt=\"\" />');\n            equal(format('img[src]/', xmlStyle), '<img src=\"\" alt=\"\"/>');\n            equal(format('div>img[src]/', xhtmlStyle), '<div><img src=\"\" alt=\"\" /></div>');\n        });\n\n        it('boolean attributes', () => {\n            const compact = createProfile({ 'output.compactBoolean': true });\n            const noCompact = createProfile({ 'output.compactBoolean': false });\n\n            equal(format('p[b.]', noCompact), '<p b=\"b\"></p>');\n            equal(format('p[b.]', compact), '<p b></p>');\n            equal(format('p[contenteditable]', compact), '<p contenteditable></p>');\n            equal(format('p[contenteditable]', noCompact), '<p contenteditable=\"contenteditable\"></p>');\n            equal(format('p[contenteditable=foo]', compact), '<p contenteditable=\"foo\"></p>');\n        });\n\n        it('no formatting', () => {\n            const profile = createProfile({ 'output.format': false });\n            equal(format('div>p', profile), '<div><p></p></div>');\n            equal(format('div>{foo}+p+{bar}', profile), '<div>foo<p></p>bar</div>');\n            equal(format('div>{foo}>p', profile), '<div>foo<p></p></div>');\n            equal(format('div>{<!-- ${0} -->}>p', profile), '<div><!-- <p></p> --></div>');\n        });\n\n        it('format specific nodes', () => {\n            equal(format('{<!DOCTYPE html>}+html>(head>meta[charset=${charset}]/+title{${1:Document}})+body', field),\n                '<!DOCTYPE html>\\n<html>\\n<head>\\n\\t<meta charset=\"utf-8\">\\n\\t<title>${2:Document}</title>\\n</head>\\n<body>\\n\\t${3}\\n</body>\\n</html>');\n        });\n\n        it('comment', () => {\n            const opt = createConfig({ options: { 'comment.enabled': true } });\n\n            equal(format('ul>li.item', opt), '<ul>\\n\\t<li class=\"item\"></li>\\n\\t<!-- /.item -->\\n</ul>');\n            equal(format('div>ul>li.item#foo', opt), '<div>\\n\\t<ul>\\n\\t\\t<li class=\"item\" id=\"foo\"></li>\\n\\t\\t<!-- /#foo.item -->\\n\\t</ul>\\n</div>');\n\n            opt.options['comment.after'] = ' { [%ID] }';\n            equal(format('div>ul>li.item#foo', opt), '<div>\\n\\t<ul>\\n\\t\\t<li class=\"item\" id=\"foo\"></li> { %foo }\\n\\t</ul>\\n</div>');\n        });\n\n        it('jsx', () => {\n            const config = createConfig({\n                syntax: 'jsx',\n                options: {\n                    'markup.attributes': {\n                        'class': 'className',\n                        'class*': 'className',\n                    },\n                    'markup.valuePrefix': {\n                        'class*': 'styles',\n                    },\n                    'output.field': (index) => `\\${${index}}`,\n                },\n            });\n\n            equal(format('.', config), '<div className=\"${1}\">${2}</div>');\n            equal(format('..', config), '<div className={${1}}>${2}</div>');\n\n            equal(format('div.', config), '<div className=\"${1}\">${2}</div>');\n            equal(format('div..', config), '<div className={${1}}>${2}</div>');\n\n            equal(format('div.a', config), '<div className=\"a\">${1}</div>');\n            equal(format('div..a', config), '<div className={styles.a}>${1}</div>');\n\n            equal(format('div.a.b', config), '<div className=\"a b\">${1}</div>');\n            equal(format('div.a..b', config), '<div className=\"a b\">${1}</div>');\n        });\n    });\n\n    describe('HAML', () => {\n        const format = (abbr: string, config = defaultConfig) => haml(parse(abbr, config), config);\n\n        it('basic', () => {\n            equal(format('div#header>ul.nav>li[title=test].nav-item*2'),\n                '#header\\n\\t%ul.nav\\n\\t\\t%li.nav-item(title=\"test\") \\n\\t\\t%li.nav-item(title=\"test\") ');\n\n            // https://github.com/emmetio/emmet/issues/446\n            equal(format('li>a'), '%li\\n\\t%a(href=\"\") ');\n\n            equal(format('div#foo[data-n1=v1 title=test data-n2=v2].bar'),\n                '#foo.bar(data-n1=\"v1\" title=\"test\" data-n2=\"v2\") ');\n\n            let profile = createProfile({ 'output.compactBoolean': true });\n            equal(format('input[disabled. foo title=test]/', profile), '%input(type=\"text\" disabled foo=\"\" title=\"test\")/');\n\n            profile = createProfile({ 'output.compactBoolean': false });\n            equal(format('input[disabled. foo title=test]/', profile), '%input(type=\"text\" disabled=true foo=\"\" title=\"test\")/');\n        });\n\n        it('nodes with text', () => {\n            equal(format('{Text 1}'), 'Text 1');\n            equal(format('span{Text 1}'), '%span Text 1');\n            equal(format('span{Text 1}>b{Text 2}'), '%span Text 1\\n\\t%b Text 2');\n            equal(format('span{Text 1\\nText 2}>b{Text 3}'), '%span\\n\\tText 1 |\\n\\tText 2 |\\n\\t%b Text 3');\n            equal(format('div>span{Text 1\\nText 2\\nText 123}>b{Text 3}'), '%div\\n\\t%span\\n\\t\\tText 1   |\\n\\t\\tText 2   |\\n\\t\\tText 123 |\\n\\t\\t%b Text 3');\n        });\n\n        it('generate fields', () => {\n            equal(format('a[href]', field), '%a(href=\"${1}\") ${2}');\n            equal(format('a[href]*2', field), '%a(href=\"${1}\") ${2}\\n%a(href=\"${3}\") ${4}');\n            equal(format('{${0} ${1:foo} ${2:bar}}*2', field), '${1} ${2:foo} ${3:bar}${4} ${5:foo} ${6:bar}');\n            equal(format('{${0} ${1:foo} ${2:bar}}*2'), ' foo bar foo bar');\n            equal(format('ul>li*2', field), '%ul\\n\\t%li ${1}\\n\\t%li ${2}');\n            equal(format('div>img[src]/', field), '%div\\n\\t%img(src=\"${1}\" alt=\"${2}\")/');\n        });\n    });\n\n    describe('Pug', () => {\n        const format = (abbr: string, config = defaultConfig) => pug(parse(abbr, config), config);\n\n        it('basic', () => {\n            equal(format('div#header>ul.nav>li[title=test].nav-item*2'),\n                '#header\\n\\tul.nav\\n\\t\\tli.nav-item(title=\"test\") \\n\\t\\tli.nav-item(title=\"test\") ');\n\n            equal(format('div#foo[data-n1=v1 title=test data-n2=v2].bar'),\n                '#foo.bar(data-n1=\"v1\", title=\"test\", data-n2=\"v2\") ');\n\n            equal(format('input[disabled. foo title=test]'), 'input(type=\"text\", disabled, foo=\"\", title=\"test\")');\n            // Use closing slash for XML output format\n            equal(format('input[disabled. foo title=test]', createProfile({ 'output.selfClosingStyle': 'xml' })), 'input(type=\"text\", disabled, foo=\"\", title=\"test\")/');\n        });\n\n        it('nodes with text', () => {\n            equal(format('{Text 1}'), 'Text 1');\n            equal(format('span{Text 1}'), 'span Text 1');\n            equal(format('span{Text 1}>b{Text 2}'), 'span Text 1\\n\\tb Text 2');\n            equal(format('span{Text 1\\nText 2}>b{Text 3}'), 'span\\n\\t| Text 1\\n\\t| Text 2\\n\\tb Text 3');\n            equal(format('div>span{Text 1\\nText 2}>b{Text 3}'), 'div\\n\\tspan\\n\\t\\t| Text 1\\n\\t\\t| Text 2\\n\\t\\tb Text 3');\n        });\n\n        it('generate fields', () => {\n            equal(format('a[href]', field), 'a(href=\"${1}\") ${2}');\n            equal(format('a[href]*2', field), 'a(href=\"${1}\") ${2}\\na(href=\"${3}\") ${4}');\n            equal(format('{${0} ${1:foo} ${2:bar}}*2', field), '${1} ${2:foo} ${3:bar}${4} ${5:foo} ${6:bar}');\n            equal(format('{${0} ${1:foo} ${2:bar}}*2'), ' foo bar foo bar');\n            equal(format('ul>li*2', field), 'ul\\n\\tli ${1}\\n\\tli ${2}');\n            equal(format('div>img[src]/', field), 'div\\n\\timg(src=\"${1}\", alt=\"${2}\")');\n        });\n    });\n\n    describe('Slim', () => {\n        const format = (abbr: string, config = defaultConfig) => slim(parse(abbr, config), config);\n\n        it('basic', () => {\n            equal(format('div#header>ul.nav>li[title=test].nav-item*2'),\n                '#header\\n\\tul.nav\\n\\t\\tli.nav-item title=\"test\" \\n\\t\\tli.nav-item title=\"test\" ');\n\n            equal(format('div#foo[data-n1=v1 title=test data-n2=v2].bar'),\n                '#foo.bar data-n1=\"v1\" title=\"test\" data-n2=\"v2\" ');\n\n            // const profile = createProfile({ inlineBreak: 0 });\n            // equal(format('ul>li>span{Text}', profile), 'ul\\n\\tli: span Text');\n            // equal(format('ul>li>span{Text}'), 'ul\\n\\tli\\n\\t\\tspan Text');\n            // equal(format('ul>li>span{Text}*2', profile), 'ul\\n\\tli\\n\\t\\tspan Text\\n\\t\\tspan Text');\n        });\n\n        // it.skip('attribute wrappers', () => {\n        //     equal(format('input[disabled. foo title=test]'), 'input disabled=true foo=\"\" title=\"test\"');\n        //     equal(format('input[disabled. foo title=test]', null, { attributeWrap: 'round' }),\n        //         'input(disabled foo=\"\" title=\"test\")');\n        // });\n\n        it('nodes with text', () => {\n            equal(format('{Text 1}'), 'Text 1');\n            equal(format('span{Text 1}'), 'span Text 1');\n            equal(format('span{Text 1}>b{Text 2}'), 'span Text 1\\n\\tb Text 2');\n            equal(format('span{Text 1\\nText 2}>b{Text 3}'), 'span\\n\\t| Text 1\\n\\t| Text 2\\n\\tb Text 3');\n            equal(format('div>span{Text 1\\nText 2}>b{Text 3}'), 'div\\n\\tspan\\n\\t\\t| Text 1\\n\\t\\t| Text 2\\n\\t\\tb Text 3');\n        });\n\n        it('generate fields', () => {\n            equal(format('a[href]', field), 'a href=\"${1}\" ${2}');\n            equal(format('a[href]*2', field), 'a href=\"${1}\" ${2}\\na href=\"${3}\" ${4}');\n            equal(format('{${0} ${1:foo} ${2:bar}}*2', field), '${1} ${2:foo} ${3:bar}${4} ${5:foo} ${6:bar}');\n            equal(format('{${0} ${1:foo} ${2:bar}}*2'), ' foo bar foo bar');\n            equal(format('ul>li*2', field), 'ul\\n\\tli ${1}\\n\\tli ${2}');\n            equal(format('div>img[src]/', field), 'div\\n\\timg src=\"${1}\" alt=\"${2}\"/');\n        });\n    });\n});\n"
  },
  {
    "path": "test/lorem.ts",
    "content": "import { describe, it } from 'node:test';\nimport { ok, strictEqual as equal } from 'node:assert';\nimport expand from '../src';\n\nfunction wordCount(str: string): number {\n    return str.split(' ').length;\n}\n\nfunction splitLines(str: string): string[] {\n    return str.split(/\\n/);\n}\n\ndescribe('Lorem Ipsum generator', () => {\n    it('single', () => {\n        let output = expand('lorem');\n        ok(/^Lorem,?\\sipsum/.test(output));\n        ok(wordCount(output) > 20);\n\n        output = expand('lorem5');\n        ok(/^Lorem,?\\sipsum/.test(output));\n        equal(wordCount(output), 5);\n\n        output = expand('lorem5-10');\n        ok(/^Lorem,?\\sipsum/.test(output));\n        ok(wordCount(output) >= 5 && wordCount(output) <= 10);\n\n        output = expand('loremru4');\n        ok(/^Далеко-далеко,?\\sза,?\\sсловесными/.test(output));\n        equal(wordCount(output), 4);\n\n        output = expand('p>lorem');\n        ok(/^<p>Lorem,?\\sipsum/.test(output));\n\n        // https://github.com/emmetio/expand-abbreviation/issues/24\n        output = expand('(p)lorem2');\n        ok(/^<p><\\/p>\\nLorem,?\\sipsum/.test(output));\n\n        output = expand('p(lorem10)');\n        ok(/^<p><\\/p>\\nLorem,?\\sipsum/.test(output));\n    });\n\n    it('multiple', () => {\n        let output = expand('lorem6*3');\n        let lines = splitLines(output);\n        ok(/^Lorem,?\\sipsum/.test(output));\n        equal(lines.length, 3);\n\n        output = expand('lorem6*2');\n        lines = splitLines(output);\n        ok(/^Lorem,?\\sipsum/.test(output));\n        equal(lines.length, 2);\n\n        output = expand('p*3>lorem');\n        lines = splitLines(output);\n        ok(/^<p>Lorem,?\\sipsum/.test(lines[0]!));\n        ok(!/^<p>Lorem,?\\sipsum/.test(lines[1]!));\n\n        output = expand('ul>lorem5*3', { options: { 'output.indent': '' } });\n        lines = splitLines(output);\n        equal(lines.length, 5);\n        ok(/^<li>Lorem,?\\sipsum/.test(lines[1]!));\n        ok(!/^<li>Lorem,?\\sipsum/.test(lines[2]!));\n    });\n});\n"
  },
  {
    "path": "test/markup.ts",
    "content": "import { describe, it } from 'node:test';\nimport { strictEqual as equal } from 'node:assert';\nimport parse from '../src/markup';\nimport resolveConfig from '../src/config';\nimport stringify from './assets/stringify';\n\nconst defaultConfig = resolveConfig({ cache: {} });\n\nfunction expand(abbr: string, config = defaultConfig): string {\n    return stringify(parse(abbr, config));\n}\n\ndescribe('Markup abbreviations', () => {\n    it('implicit tags', () => {\n        equal(expand('.'), '<div class=\"\"></div>');\n        equal(expand('.foo>.bar'), '<div class=\"foo\"><div class=\"bar\"></div></div>');\n        equal(expand('p.foo>.bar'), '<p class=\"foo\"><span class=\"bar\"></span></p>');\n        equal(expand('ul>.item*2'), '<ul><li*2@0 class=\"item\"></li><li*2@1 class=\"item\"></li></ul>');\n        equal(expand('table>.row>.cell'), '<table><tr class=\"row\"><td class=\"cell\"></td></tr></table>');\n        equal(expand('{test}'), 'test');\n        equal(expand('.{test}'), '<div class=\"\">test</div>');\n        equal(expand('ul>.item$*2'), '<ul><li*2@0 class=\"item1\"></li><li*2@1 class=\"item2\"></li></ul>');\n    });\n\n    it('XSL', () => {\n        const config = resolveConfig({ syntax: 'xsl' });\n        equal(expand('xsl:variable[select]', config), '<xsl:variable select=\"\"></xsl:variable>');\n        equal(expand('xsl:with-param[select]', config), '<xsl:with-param select=\"\"></xsl:with-param>');\n        equal(expand('xsl:variable[select]>div', config), '<xsl:variable><div></div></xsl:variable>');\n        equal(expand('xsl:with-param[select]{foo}', config), '<xsl:with-param>foo</xsl:with-param>');\n    });\n\n    it('<label> preprocessor', () => {\n        equal(expand('label>input'), '<label><input type=\"${1:text}\" /></label>');\n        equal(expand('label>inp'), '<label><input type=\"${1:text}\" name=\"${1}\" /></label>');\n        equal(expand('label>span>input'), '<label><span><input type=\"${1:text}\" /></span></label>');\n        equal(expand('label+inp'), '<label for=\"\"></label><input type=\"${1:text}\" name=\"${1}\" id=\"${1}\" />');\n    });\n\n    describe('BEM transform', () => {\n        const config = resolveConfig({\n            options: { 'bem.enabled': true }\n        });\n\n        it('modifiers', () => {\n            equal(expand('div.b_m', config), '<div class=\"b b_m\"></div>');\n            equal(expand('div.b._m', config), '<div class=\"b b_m\"></div>');\n            equal(expand('div.b_m1._m2', config), '<div class=\"b b_m1 b_m2\"></div>');\n            equal(expand('div.b>div._m', config), '<div class=\"b\"><div class=\"b b_m\"></div></div>');\n            equal(expand('div.b>div._m1>div._m2', config), '<div class=\"b\"><div class=\"b b_m1\"><div class=\"b b_m2\"></div></div></div>');\n\n            // classnames with -\n            equal(expand('div.b>div._m1-m2', config), '<div class=\"b\"><div class=\"b b_m1-m2\"></div></div>');\n        });\n\n        it('elements', () => {\n            equal(expand('div.b>div.-e', config), '<div class=\"b\"><div class=\"b__e\"></div></div>');\n            equal(expand('div.b>div.---e', config), '<div class=\"b\"><div class=\"b__e\"></div></div>');\n            equal(expand('div.b>div.-e>div.-e', config), '<div class=\"b\"><div class=\"b__e\"><div class=\"b__e\"></div></div></div>');\n            equal(expand('div', config), '<div></div>', 'Fixes bug with empty class');\n\n            // get block name from proper ancestor\n            equal(expand('div.b1>div.b2_m1>div.-e1+div.---e2_m2', config),\n                '<div class=\"b1\"><div class=\"b2 b2_m1\"><div class=\"b2__e1\"></div><div class=\"b1__e2 b1__e2_m2\"></div></div></div>');\n\n            // class names with -\n            equal(expand('div.b>div.-m1-m2', config), '<div class=\"b\"><div class=\"b__m1-m2\"></div></div>');\n\n            // class names with _\n            equal(expand('div.b_m_o', config), '<div class=\"b b_m_o\"></div>');\n        });\n\n        it('customize modifier', () => {\n            const localConfig = resolveConfig({\n                options: {\n                    'bem.enabled': true,\n                    'bem.element': '-',\n                    'bem.modifier': '__'\n                }\n            });\n            equal(expand('div.b_m', localConfig), '<div class=\"b b__m\"></div>');\n            equal(expand('div.b._m', localConfig), '<div class=\"b b__m\"></div>');\n        });\n\n        it('multiple classes after modifier/element', () => {\n            equal(expand('div.b_m.c', config), '<div class=\"b b_m c\"></div>');\n            equal(expand('div.b>div._m.c', config), '<div class=\"b\"><div class=\"b b_m c\"></div></div>');\n            equal(expand('div.b>div.-m.c', config), '<div class=\"b\"><div class=\"b__m c\"></div></div>');\n        });\n\n        it('parent context', () => {\n            // Get block name from context\n            equal(expand('.-e_m', resolveConfig({\n                context: { name: 'div', attributes: { class: 'bl' } },\n                options: { 'bem.enabled': true }\n            })), '<div class=\"bl__e bl__e_m\"></div>');\n        });\n    });\n});\n"
  },
  {
    "path": "test/output.ts",
    "content": "import { describe, it } from 'node:test';\nimport { strictEqual as equal } from 'node:assert';\nimport createStream, { push, pushString, pushNewline, tagName, attrName, selfClose, isInline } from '../src/output-stream';\nimport createConfig, { defaultOptions } from '../src/config';\n\ndescribe('Output', () => {\n    it('stream', () => {\n        const out = createStream({\n            ...defaultOptions,\n            'output.baseIndent': '>>'\n        });\n\n        push(out, 'aaa');\n        equal(out.value, 'aaa');\n        equal(out.line, 0);\n        equal(out.column, 3);\n        equal(out.offset, 3);\n\n        pushString(out, 'bbb');\n        equal(out.value, 'aaabbb');\n        equal(out.line, 0);\n        equal(out.column, 6);\n        equal(out.offset, 6);\n\n        // Add text with newlines\n        pushString(out, 'ccc\\nddd');\n        equal(out.value, 'aaabbbccc\\n>>ddd');\n        equal(out.line, 1);\n        equal(out.column, 5);\n        equal(out.offset, 15);\n\n        // Add newline with indent\n        out.level++;\n        pushNewline(out, true);\n        equal(out.value, 'aaabbbccc\\n>>ddd\\n>>\\t');\n        equal(out.line, 2);\n        equal(out.column, 3);\n        equal(out.offset, 19);\n    });\n\n    describe('Output profile', () => {\n        it('tag name', () => {\n            const asis = createConfig({ options: { 'output.tagCase': '' } });\n            const upper = createConfig({ options: { 'output.tagCase': 'upper' } });\n            const lower = createConfig({ options: { 'output.tagCase': 'lower' } });\n\n            equal(tagName('Foo', asis), 'Foo');\n            equal(tagName('bAr', asis), 'bAr');\n\n            equal(tagName('Foo', upper), 'FOO');\n            equal(tagName('bAr', upper), 'BAR');\n\n            equal(tagName('Foo', lower), 'foo');\n            equal(tagName('bAr', lower), 'bar');\n        });\n\n        it('attribute name', () => {\n            const asis = createConfig({ options: { 'output.attributeCase': '' } });\n            const upper = createConfig({ options: { 'output.attributeCase': 'upper' } });\n            const lower = createConfig({ options: { 'output.attributeCase': 'lower' } });\n\n            equal(attrName('Foo', asis), 'Foo');\n            equal(attrName('bAr', asis), 'bAr');\n\n            equal(attrName('Foo', upper), 'FOO');\n            equal(attrName('bAr', upper), 'BAR');\n\n            equal(attrName('Foo', lower), 'foo');\n            equal(attrName('bAr', lower), 'bar');\n        });\n\n        it('self close', () => {\n            const html = createConfig({ options: { 'output.selfClosingStyle': 'html' } });\n            const xhtml = createConfig({ options: { 'output.selfClosingStyle': 'xhtml' } });\n            const xml = createConfig({ options: { 'output.selfClosingStyle': 'xml' } });\n\n            equal(selfClose(html), '');\n            equal(selfClose(xhtml), ' /');\n            equal(selfClose(xml), '/');\n        });\n\n        it('inline elements', () => {\n            const config = createConfig();\n            equal(isInline('a', config), true);\n            equal(isInline('b', config), true);\n            equal(isInline('c', config), false);\n        });\n    });\n});\n"
  },
  {
    "path": "test/snippets.ts",
    "content": "import { describe, it } from 'node:test';\nimport { ok, strictEqual as equal } from 'node:assert';\nimport markup from '@emmetio/abbreviation';\nimport expand from '../src';\nimport html from '../src/snippets/html.json' assert { type: 'json' };\nimport xsl from '../src/snippets/xsl.json' assert { type: 'json' };\n\ndescribe('Snippets', () => {\n    it('HTML', () => {\n        Object.keys(html).forEach(k => ok(markup(html[k]), k));\n        equal(expand('fset>input:c'), '<fieldset><input type=\"checkbox\" name=\"\" id=\"\"></fieldset>');\n    });\n\n    it('XSL', () => {\n        Object.keys(xsl).forEach(k => ok(markup(xsl[k]), k));\n    });\n\n    it('Invalid snippets', () => {\n        const snippets = {\n            invalid: 'invalid snippet',\n            valid: 'button'\n        }\n\n        const result = expand('invalid+valid', { snippets })\n        equal(result, '<invalid></invalid>\\n<button></button>')\n    });\n});\n"
  },
  {
    "path": "test/stylesheet.ts",
    "content": "import { describe, it } from 'node:test';\nimport { strictEqual as equal, ok } from 'node:assert';\nimport { stylesheet as expandAbbreviation, resolveConfig, CSSAbbreviationScope } from '../src';\nimport score from '../src/stylesheet/score';\n\nconst defaultConfig = resolveConfig({\n    type: 'stylesheet',\n    options: {\n        'output.field': (index, placeholder) => `\\${${index}${placeholder ? ':' + placeholder : ''}}`,\n        'stylesheet.fuzzySearchMinScore': 0\n    },\n    snippets: {\n        mten: 'margin: 10px;',\n        fsz: 'font-size',\n        gt: 'grid-template: repeat(2,auto) / repeat(auto-fit, minmax(250px, 1fr))',\n        bxsh: 'box-shadow: var(--bxsh-${1})'\n    },\n    cache: {},\n});\n\nfunction expand(abbr: string, config = defaultConfig) {\n    return expandAbbreviation(abbr, config);\n}\n\ndescribe('Stylesheet abbreviations', () => {\n    describe('Scoring', () => {\n        const pick = (abbr: string, items: string[]) => items\n            .map(item => ({ item, score: score(abbr, item, true) }))\n            .filter(obj => obj.score)\n            .sort((a, b) => b.score - a.score)\n            .map(obj => obj.item)[0];\n\n        it('compare scores', () => {\n            equal(score('aaa', 'aaa'), 1);\n            equal(score('baa', 'aaa'), 0);\n\n            ok(!score('b', 'aaa'));\n            ok(score('a', 'aaa'));\n            ok(score('a', 'abc'));\n            ok(score('ac', 'abc'));\n            ok(score('a', 'aaa') < score('aa', 'aaa'));\n            ok(score('ab', 'abc') > score('ab', 'acb'));\n\n            // acronym bonus\n            ok(score('ab', 'a-b') > score('ab', 'acb'));\n        });\n\n        it('pick padding or position', () => {\n            const items = ['p', 'pb', 'pl', 'pos', 'pa', 'oa', 'soa', 'pr', 'pt'];\n\n            equal(pick('p', items), 'p');\n            equal(pick('poa', items), 'pos');\n        });\n    });\n\n    it('keywords', () => {\n        equal(expand('bd1-s'), 'border: 1px solid;');\n        equal(expand('dib'), 'display: inline-block;');\n        equal(expand('bxsz'), 'box-sizing: ${1:border-box};');\n        equal(expand('bxz'), 'box-sizing: ${1:border-box};');\n        equal(expand('bxzc'), 'box-sizing: content-box;');\n        equal(expand('fl'), 'float: ${1:left};');\n        equal(expand('fll'), 'float: left;');\n\n        equal(expand('pos'), 'position: ${1:relative};');\n        equal(expand('poa'), 'position: absolute;');\n        equal(expand('por'), 'position: relative;');\n        equal(expand('pof'), 'position: fixed;');\n        equal(expand('pos-a'), 'position: absolute;');\n\n        equal(expand('m'), 'margin: ${0};');\n        equal(expand('m0'), 'margin: 0;');\n\n        // use `auto` as global keyword\n        equal(expand('m0-a'), 'margin: 0 auto;');\n        equal(expand('m-a'), 'margin: auto;');\n\n        equal(expand('bg'), 'background: ${1:#000};');\n\n        equal(expand('bd'), 'border: ${1:1px} ${2:solid} ${3:#000};');\n        equal(expand('bd0-s#fc0'), 'border: 0 solid #fc0;');\n        equal(expand('bd0-dd#fc0'), 'border: 0 dot-dash #fc0;');\n        equal(expand('bd0-h#fc0'), 'border: 0 hidden #fc0;');\n\n        equal(expand('trf-trs'), 'transform: translate(${1:x}, ${2:y});');\n\n        // https://github.com/emmetio/emmet/issues/610\n        equal(expand('c'), 'color: ${1:#000};');\n        equal(expand('cr'), 'color: rgb(${1:0}, ${2:0}, ${3:0});');\n        equal(expand('cra'), 'color: rgba(${1:0}, ${2:0}, ${3:0}, ${4:.5});');\n\n        // https://github.com/emmetio/emmet/issues/647\n        equal(expand('gtc'), 'grid-template-columns: repeat(${0});');\n        equal(expand('gtr'), 'grid-template-rows: repeat(${0});');\n\n        equal(expand('lis:n'), 'list-style: none;');\n        equal(expand('list:n'), 'list-style-type: none;');\n        equal(expand('bdt:n'), 'border-top: none;');\n        equal(expand('bgi:n'), 'background-image: none;');\n        equal(expand('q:n'), 'quotes: none;');\n\n        // https://github.com/emmetio/emmet/issues/628\n        equal(expand('bg:n'), 'background: none;');\n\n        // https://github.com/emmetio/emmet/issues/691\n        equal(expand('d:c'), 'display: contents;');\n\n        // Custom properties\n        // https://github.com/emmetio/emmet/issues/692\n        equal(expand('bxsh'), 'box-shadow: var(--bxsh-${1});');\n    });\n\n    it('numeric', () => {\n        equal(expand('p0'), 'padding: 0;', 'No unit for 0');\n        equal(expand('p10'), 'padding: 10px;', '`px` unit for integers');\n        equal(expand('p.4'), 'padding: 0.4em;', '`em` for floats');\n        equal(expand('fz10'), 'font-size: 10px;', '`px` for integers');\n        equal(expand('fz1.'), 'font-size: 1em;', '`em` for explicit float');\n        equal(expand('p10p'), 'padding: 10%;', 'unit alias');\n        equal(expand('z10'), 'z-index: 10;', 'Unitless property');\n        equal(expand('p10r'), 'padding: 10rem;', 'unit alias');\n        equal(expand('mten'), 'margin: 10px;', 'Ignore terminating `;` in snippet');\n\n        // https://github.com/microsoft/vscode/issues/59951\n        equal(expand('fz'), 'font-size: ${0};');\n        equal(expand('fz12'), 'font-size: 12px;');\n        equal(expand('fsz'), 'font-size: ${0};');\n        equal(expand('fsz12'), 'font-size: 12px;');\n        equal(expand('fs'), 'font-style: ${1:italic};');\n\n        // https://github.com/emmetio/emmet/issues/558\n        equal(expand('us'), 'user-select: none;');\n\n        // https://github.com/microsoft/vscode/issues/105697\n        equal(expand('opa1'), 'opacity: 1;', 'Unitless property');\n        equal(expand('opa.1'), 'opacity: 0.1;', 'Unitless property');\n        equal(expand('opa.a'), 'opacity: .a;', 'Unitless property');\n    });\n\n    it('numeric with format options', () => {\n        const config = resolveConfig({\n            type: 'stylesheet',\n            options: {\n                'stylesheet.intUnit': 'pt',\n                'stylesheet.floatUnit': 'vh',\n                'stylesheet.unitAliases': {\n                    e: 'em',\n                    p: '%',\n                    x: 'ex',\n                    r: ' / @rem'\n                }\n            }\n        });\n        equal(expand('p0', config), 'padding: 0;', 'No unit for 0');\n        equal(expand('p10', config), 'padding: 10pt;', '`pt` unit for integers');\n        equal(expand('p.4', config), 'padding: 0.4vh;', '`vh` for floats');\n        equal(expand('p10p', config), 'padding: 10%;', 'unit alias');\n        equal(expand('z10', config), 'z-index: 10;', 'Unitless property');\n        equal(expand('p10r', config), 'padding: 10 / @rem;', 'unit alias');\n    });\n\n    it('important', () => {\n        equal(expand('!'), '!important');\n        equal(expand('p!'), 'padding: ${0} !important;');\n        equal(expand('p0!'), 'padding: 0 !important;');\n    });\n\n    it('color', () => {\n        equal(expand('c'), 'color: ${1:#000};');\n        equal(expand('c#'), 'color: #000;');\n        equal(expand('c#f.5'), 'color: rgba(255, 255, 255, 0.5);');\n        equal(expand('c#f.5!'), 'color: rgba(255, 255, 255, 0.5) !important;');\n        equal(expand('bgc'), 'background-color: ${1:#fff};');\n    });\n\n    it('snippets', () => {\n        equal(expand('@'), '@media ${1:screen} {\\n\\t${0}\\n}');\n\n        // Insert value into snippet fields\n        equal(expand('@k-name'), '@keyframes name {\\n\\t${2}\\n}');\n        equal(expand('@k-name10'), '@keyframes name {\\n\\t10\\n}');\n        equal(expand('gt'), 'grid-template: repeat(2, auto) / repeat(auto-fit, minmax(250px, 1fr));');\n    });\n\n    it('multiple properties', () => {\n        equal(expand('p10+m10-20'), 'padding: 10px;\\nmargin: 10px 20px;');\n        equal(expand('p+bd'), 'padding: ${0};\\nborder: ${1:1px} ${2:solid} ${3:#000};');\n    });\n\n    it('functions', () => {\n        equal(expand('trf-s(2)'), 'transform: scale(2, ${2:y});');\n        equal(expand('trf-s(2, 3)'), 'transform: scale(2, 3);');\n    });\n\n    it('case insensitive matches', () => {\n        equal(expand('trf:rx'), 'transform: rotateX(${1:angle});');\n    });\n\n    it('gradient resolver', () => {\n        equal(expand('lg'), 'background-image: linear-gradient(${0});');\n        equal(expand('lg(to right, #0, #f00.5)'), 'background-image: linear-gradient(to right, #000, rgba(255, 0, 0, 0.5));');\n    });\n\n    it('unmatched abbreviation', () => {\n        // This example is useless: it’s unexpected to receive `align-self: unset`\n        // for `auto` snippet\n        // equal(expand('auto', resolveConfig({\n        //     type: 'stylesheet',\n        //     options: { 'stylesheet.fuzzySearchMinScore': 0 }\n        // })), 'align-self: unset;');\n        equal(expand('auto'), 'auto: ${0};');\n    });\n\n    it('CSS-in-JS', () => {\n        const config = resolveConfig({\n            type: 'stylesheet',\n            options: {\n                'stylesheet.json': true,\n                'stylesheet.between': ': '\n            }\n        });\n\n        equal(expand('p10+mt10-20', config), 'padding: 10,\\nmarginTop: \\'10px 20px\\',');\n        equal(expand('bgc', config), 'backgroundColor: \\'#fff\\',');\n    });\n\n    it('resolve context value', () => {\n        const config = resolveConfig({\n            type: 'stylesheet',\n            context: { name: 'align-content' }\n        });\n\n        equal(expand('s', config), 'start');\n        equal(expand('a', config), 'auto');\n    });\n\n    it('limit snippets by scope', () => {\n        const sectionScope = resolveConfig({\n            type: 'stylesheet',\n            context: { name: CSSAbbreviationScope.Section },\n            snippets: {\n                mten: 'margin: 10px;',\n                fsz: 'font-size',\n                myCenterAwesome: 'body {\\n\\tdisplay: grid;\\n}'\n            }\n        });\n        const propertyScope = resolveConfig({\n            type: 'stylesheet',\n            context: { name: CSSAbbreviationScope.Property },\n            snippets: {\n                mten: 'margin: 10px;',\n                fsz: 'font-size',\n                myCenterAwesome: 'body {\\n\\tdisplay: grid;\\n}'\n            }\n        });\n\n        equal(expand('m', sectionScope), 'body {\\n\\tdisplay: grid;\\n}');\n        equal(expand('b', sectionScope), '');\n        equal(expand('m', propertyScope), 'margin: ;');\n    });\n\n    it('Logical Properties', () => {\n        equal(expand('bbs'),'border-block-start: ${0};');\n        equal(expand('bbe\"'),'border-block-end: \"\";');\n        equal(expand('bis'),'border-inline-start: ${0};');\n        equal(expand('bie'),'border-inline-end: ${0};');\n        equal(expand('bs'),'block-size: ${0};');\n        equal(expand('is'),'inline-size: ${0};');\n        equal(expand('mbs'),'margin-block-start: ${0};');\n        equal(expand('mbe'),'margin-block-end: ${0};');\n        equal(expand('mis'),'margin-inline-start: ${0};');\n        equal(expand('mie'),'margin-inline-end: ${0};');\n        equal(expand('pbs'),'padding-block-start: ${0};');\n        equal(expand('pbe'),'padding-block-end: ${0};');\n        equal(expand('pis'),'padding-inline-start: ${0};');\n        equal(expand('pie'),'padding-inline-end: ${0};');\n        equal(expand('spbs'),'scroll-padding-block-start: ${0};');\n        equal(expand('spbe'),'scroll-padding-block-end: ${0};');\n        equal(expand('spis'),'scroll-padding-inline-start: ${0};');\n        equal(expand('spie'),'scroll-padding-inline-end: ${0};');\n\n    });\n\n    it('strict match', () => {\n        const config = resolveConfig({\n            type: 'stylesheet',\n            options: {\n                'stylesheet.strictMatch': true\n            }\n        });\n        equal(expand('max-inline-size:'), 'max: inline size;')\n        equal(expand('max-inline-size:', config), '')\n    })\n});\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2017\",\n    \"sourceMap\": true,\n    \"noUnusedLocals\": true,\n    \"alwaysStrict\": true,\n    \"noImplicitThis\": true,\n    \"strictNullChecks\": true,\n    \"isolatedModules\": true,\n    \"declaration\": true,\n    \"resolveJsonModule\": true,\n    \"esModuleInterop\": true,\n    \"verbatimModuleSyntax\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"newLine\": \"LF\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"]\n}\n"
  }
]