Showing preview only (369K chars total). Download the full file or copy to clipboard to get everything.
Repository: emmetio/emmet
Branch: master
Commit: 9f0944960ec3
Files: 105
Total size: 343.5 KB
Directory structure:
gitextract_u8cxec12/
├── .editorconfig
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ └── node.js.yml
├── .gitignore
├── .npmignore
├── .npmrc
├── LICENSE
├── README.md
├── lerna.json
├── package.json
├── packages/
│ ├── abbreviation/
│ │ ├── .npmignore
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── rollup.config.js
│ │ ├── src/
│ │ │ ├── convert.ts
│ │ │ ├── index.ts
│ │ │ ├── parser/
│ │ │ │ ├── TokenScanner.ts
│ │ │ │ └── index.ts
│ │ │ ├── stringify.ts
│ │ │ ├── tokenizer/
│ │ │ │ ├── index.ts
│ │ │ │ ├── tokens.ts
│ │ │ │ └── utils.ts
│ │ │ └── types.ts
│ │ ├── test/
│ │ │ ├── assets/
│ │ │ │ ├── stringify-node.ts
│ │ │ │ └── stringify.ts
│ │ │ ├── convert.ts
│ │ │ ├── parser.ts
│ │ │ └── tokenizer.ts
│ │ └── tsconfig.json
│ ├── css-abbreviation/
│ │ ├── .npmignore
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── rollup.config.js
│ │ ├── src/
│ │ │ ├── index.ts
│ │ │ ├── parser/
│ │ │ │ ├── TokenScanner.ts
│ │ │ │ └── index.ts
│ │ │ └── tokenizer/
│ │ │ ├── index.ts
│ │ │ ├── tokens.ts
│ │ │ └── utils.ts
│ │ ├── test/
│ │ │ ├── assets/
│ │ │ │ └── stringify.ts
│ │ │ ├── parser.ts
│ │ │ └── tokenizer.ts
│ │ └── tsconfig.json
│ └── scanner/
│ ├── .gitignore
│ ├── .npmignore
│ ├── LICENSE
│ ├── package.json
│ ├── rollup.config.js
│ ├── src/
│ │ ├── scanner.ts
│ │ └── utils.ts
│ ├── test/
│ │ ├── stream-reader.ts
│ │ └── utils.ts
│ └── tsconfig.json
├── rollup.config.js
├── src/
│ ├── config.ts
│ ├── extract-abbreviation/
│ │ ├── brackets.ts
│ │ ├── index.ts
│ │ ├── is-html.ts
│ │ ├── quotes.ts
│ │ └── reader.ts
│ ├── index.ts
│ ├── markup/
│ │ ├── addon/
│ │ │ ├── bem.ts
│ │ │ ├── label.ts
│ │ │ └── xsl.ts
│ │ ├── attributes.ts
│ │ ├── format/
│ │ │ ├── comment.ts
│ │ │ ├── haml.ts
│ │ │ ├── html.ts
│ │ │ ├── indent-format.ts
│ │ │ ├── pug.ts
│ │ │ ├── slim.ts
│ │ │ ├── template.ts
│ │ │ ├── utils.ts
│ │ │ └── walk.ts
│ │ ├── implicit-tag.ts
│ │ ├── index.ts
│ │ ├── lorem/
│ │ │ ├── index.ts
│ │ │ ├── latin.json
│ │ │ ├── russian.json
│ │ │ └── spanish.json
│ │ ├── snippets.ts
│ │ └── utils.ts
│ ├── output-stream.ts
│ ├── snippets/
│ │ ├── css.json
│ │ ├── html.json
│ │ ├── pug.json
│ │ ├── variables.json
│ │ └── xsl.json
│ └── stylesheet/
│ ├── color.ts
│ ├── format.ts
│ ├── index.ts
│ ├── score.ts
│ └── snippets.ts
├── test/
│ ├── assets/
│ │ └── stringify.ts
│ ├── expand.ts
│ ├── extract-abbreviation.ts
│ ├── format.ts
│ ├── lorem.ts
│ ├── markup.ts
│ ├── output.ts
│ ├── snippets.ts
│ └── stylesheet.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{json,yml}]
indent_style = space
indent_size = 2
[snippets/*.json]
indent_style = tab
indent_size = 4
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: emmetio
patreon: # Replace with a single Patreon username
open_collective: emmet
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
================================================
FILE: .github/workflows/node.js.yml
================================================
# 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
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: Node.js CI
on:
push:
branches: [ "master", "ci" ]
pull_request:
branches: [ "master" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x, 22.x, 24.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run build:full
- run: npm run test:all
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
.rpt2_cache
dist/
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
.DS_Store
/.vscode
================================================
FILE: .npmignore
================================================
npm-debug.log*
node_modules
jspm_packages
.npm
/.*
/*.*
/test
/src
/packages
================================================
FILE: .npmrc
================================================
save-exact=true
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020 Sergey Chikuyonok <serge.che@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Emmet — the essential toolkit for web-developers
Emmet is a web-developer’s toolkit for boosting HTML & CSS code writing.
With Emmet, you can type expressions (_abbreviations_) similar to CSS selectors and convert them into code fragment with a single keystroke. For example, this abbreviation:
```
ul#nav>li.item$*4>a{Item $}
```
...can be expanded into:
```html
<ul id="nav">
<li class="item1"><a href="">Item 1</a></li>
<li class="item2"><a href="">Item 2</a></li>
<li class="item3"><a href="">Item 3</a></li>
<li class="item4"><a href="">Item 4</a></li>
</ul>
```
## Features
* **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.
* **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.
* **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)`.
* **Available for most popular syntaxes**: use single abbreviation to produce code for most popular syntaxes like HAML, Pug, JSX, SCSS, SASS etc.
[Read more about Emmet features](https://docs.emmet.io)
This repo contains only core module for parsing and expanding Emmet abbreviations. Editor plugins are available as [separate repos](https://github.com/emmetio).
This 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).
### Installation
You can install Emmet as a regular npm module:
```bash
npm i emmet
```
## Usage
To expand abbreviation, pass it to default function of `emmet` module:
```js
import expand from 'emmet';
console.log(expand('p>a')); // <p><a href=""></a></p>
```
By 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:
```js
import expand from 'emmet';
console.log(expand('p10', { type: 'stylesheet' })); // padding: 10px;
```
A stylesheet abbreviation has slightly different syntax compared to markup one: it doesn’t support nesting and attributes but allows embedded values in element name.
Alternatively, Emmet supports *syntaxes* with predefined snippets and options:
```js
import expand from 'emmet';
console.log(expand('p10', { syntax: 'css' })); // padding: 10px;
console.log(expand('p10', { syntax: 'stylus' })); // padding 10px
```
Predefined 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`):
```js
import expand from 'emmet';
console.log(expand('p10', {
syntax: 'my-custom-syntax',
type: 'stylesheet',
options: {
'stylesheet.between': '__',
'stylesheet.after': '',
}
})); // padding__10px
```
You 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.
## Extracting abbreviations from text
A 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:
```js
import expand, { extract } from 'emmet';
const source = 'Hello world ul.tabs>li';
const data = extract(source, 22); // { abbreviation: 'ul.tabs>li' }
console.log(expand(data.abbreviation)); // <ul class="tabs"><li></li></ul>
```
The `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.
Most 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):
```
ul>li[title="Foo|"]
```
E.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:
```js
import { extract } from 'emmet';
const source = 'a div[title] b';
const loc = 11; // right after "title" word
// `lookAhead` is enabled by default
console.log(extract(source, loc)); // { abbreviation: 'div[title]', start: 2, end: 12 }
console.log(extract(source, loc, { lookAhead: false })); // { abbreviation: 'title', start: 6, end: 11 }
```
By 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:
```js
import { extract } from 'emmet';
const source = 'a{b}';
const loc = 3; // right after "b"
console.log(extract(source, loc)); // { abbreviation: 'a{b}', start: 0, end: 4 }
// Stylesheet abbreviations does not have `{text}` syntax
console.log(extract(source, loc, { type: 'stylesheet' })); // { abbreviation: 'b', start: 2, end: 3 }
```
### Extract abbreviation with custom prefix
Lots 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.:
```js
var div // `div` is a valid abbreviation, Emmet may transform it to `<div></div>`
```
A possible solution for this problem it to use _prefix_ for abbreviation: abbreviation can be successfully extracted only if its preceded with given prefix.
```js
import { extract } from 'emmet';
const source1 = '() => div';
const source2 = '() => <div';
extract(source1, source1.length); // Finds `div` abbreviation
extract(source2, source2.length); // Finds `div` abbreviation too
extract(source1, source1.length, { prefix: '<' }); // No match, `div` abbreviation is not preceded with `<` prefix
extract(source2, source2.length, { prefix: '<' }); // Finds `div` since it preceded with `<` prefix
```
With `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.
================================================
FILE: lerna.json
================================================
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "independent"
}
================================================
FILE: package.json
================================================
{
"name": "emmet",
"version": "2.4.11",
"description": "Emmet — the essential toolkit for web-developers",
"main": "./dist/emmet.cjs",
"module": "./dist/emmet.js",
"types": "./dist/index.d.ts",
"type": "module",
"exports": {
"import": "./dist/emmet.js",
"require": "./dist/emmet.cjs"
},
"scripts": {
"build": "rollup -c",
"build:packages": "npm run build --if-present --workspaces",
"build:full": "npm run build:packages && npm run build",
"watch": "rollup -wc",
"test": "tsx --test ./test/*.ts",
"test:all": "npm run test --workspaces && npm run test",
"clean": "rimraf ./dist",
"prepublishOnly": "npm run clean && npm run build:full"
},
"repository": {
"type": "git",
"url": "git+https://github.com/emmetio/emmet.git"
},
"keywords": [
"emmet",
"html",
"css",
"snippets",
"coding"
],
"author": "Sergey Chikuyonok <serge.che@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/emmetio/emmet/issues"
},
"homepage": "https://github.com/emmetio/emmet#readme",
"devDependencies": {
"@emmetio/abbreviation": "workspace:",
"@emmetio/css-abbreviation": "workspace:",
"@rollup/plugin-node-resolve": "16.0.3",
"@rollup/plugin-typescript": "12.2.0",
"@types/node": "22.10.1",
"lerna": "9.0.0",
"rimraf": "6.0.1",
"rollup": "4.52.5",
"tsx": "4.20.6",
"typescript": "5.9.3"
},
"workspaces": [
"./packages/scanner",
"./packages/abbreviation",
"./packages/css-abbreviation"
]
}
================================================
FILE: packages/abbreviation/.npmignore
================================================
npm-debug.log*
node_modules
jspm_packages
.npm
/.*
/*.*
/test
/src
================================================
FILE: packages/abbreviation/LICENSE
================================================
MIT License
Copyright (c) 2020 Sergey Chikuyonok <serge.che@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: packages/abbreviation/README.md
================================================
# Emmet markup abbreviation parser
Parses 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.
Note 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).
## Usage
You can install it via npm:
```bash
npm install @emmetio/abbreviation
```
Then add it into your project:
```js
import parse from '@emmetio/abbreviation';
const tree = parse('div#foo>span.bar*3');
/* {
type: 'Abbreviation',
children: [{
type: 'AbbreviationNode',
name: 'div',
attributes: [...],
children: [...]
}]
} */
```
The 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.
## Abbreviation syntax
Emmet abbreviation element has the following basic parts:
```
name.class#id[attributes?, ...]{text value}*repeater/
```
* `name` — element name, like `div`, `span` etc. Stored as `node.name` property.
* `[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:
* `attr` — attribute with empty value.
* `attr=value` — attribute with value. The `value` may contain any character except space or `]`.
* `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.
* `attr.` — boolean attribute, e.g. attribute without value, like `required` in `<input>`.
* `!attr` – implicit attribute, will be outputted if its value is not empty. Used as a placeholder to preserve attribute order in output.
* `./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.
* `.class` — shorthand for `class` attribute. Note that an element can have multiple classes, like `.class1.class2.class3`.
* `#id` — shorthand for `id` attribute.
* `{text}` — node’s text content
* `*N` — element repeater, tells parser to create `N` copies of given node.
* `/` — optional self-closing operator. Marks element with `node.selfClosing = true`.
### Operators
Each element of abbreviation must be separated with any of these operators:
```
elem1+elem2>elem3
```
* `+` — sibling operator, adds next element as a next sibling of current element in tree.
* `>` — child operator, adds next element as a child of current element.
* `^` — 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.
### Groups
A set of elements could be grouped using `()`, mostly for repeating and for easier elements nesting:
```
a>(b>c+d)*4+(e+f)
```
Groups can be optionally concatenated with `+` operator.
================================================
FILE: packages/abbreviation/package.json
================================================
{
"name": "@emmetio/abbreviation",
"version": "2.3.3",
"description": "Emmet standalone abbreviation parser",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"type": "module",
"exports": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"scripts": {
"test": "tsx --test ./test/*.ts",
"build": "rollup -c",
"watch": "rollup -wc",
"clean": "rm -rf ./dist",
"prepublishOnly": "npm test && npm run clean && npm run build"
},
"keywords": [
"emmet",
"abbreviation"
],
"author": "Sergey Chikuyonok <serge.che@gmail.com>",
"license": "MIT",
"dependencies": {
"@emmetio/scanner": "^1.0.4"
},
"directories": {
"test": "test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/emmetio/emmet.git"
},
"bugs": {
"url": "https://github.com/emmetio/emmet/issues"
},
"homepage": "https://github.com/emmetio/emmet#readme"
}
================================================
FILE: packages/abbreviation/rollup.config.js
================================================
import typescript from '@rollup/plugin-typescript';
export default {
input: './src/index.ts',
external: ['@emmetio/scanner'],
plugins: [typescript()],
output: [{
format: 'cjs',
sourcemap: true,
exports: 'named',
file: './dist/index.cjs'
}, {
format: 'es',
sourcemap: true,
file: './dist/index.js'
}]
};
================================================
FILE: packages/abbreviation/src/convert.ts
================================================
import { isQuote, isBracket } from './parser';
import type { TokenGroup, TokenStatement, TokenElement, TokenAttribute } from './parser';
import type { Abbreviation, ParserOptions, AbbreviationNode, ConvertState, Value, AbbreviationAttribute, AttributeType } from './types';
import type { Repeater, ValueToken, Quote, Field } from './tokenizer';
import stringify from './stringify';
const urlRegex = /^((https?:|ftp:|file:)?\/\/|(www|ftp)\.)[^ ]*$/;
const emailRegex = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,5}$/;
/**
* Converts given token-based abbreviation into simplified and unrolled node-based
* abbreviation
*/
export default function convert(abbr: TokenGroup, options: ParserOptions = {}): Abbreviation {
let textInserted = false;
let cleanText: string | string[] | undefined;
if (options.text) {
if (Array.isArray(options.text)) {
cleanText = options.text.filter(s => s.trim());
} else {
cleanText = options.text;
}
}
const result: Abbreviation = {
type: 'Abbreviation',
children: convertGroup(abbr, {
inserted: false,
repeaters: [],
text: options.text,
cleanText,
repeatGuard: options.maxRepeat || Number.POSITIVE_INFINITY,
getText(pos) {
textInserted = true;
let value: string;
if (Array.isArray(options.text)) {
if (pos !== undefined && pos >= 0 && pos < cleanText!.length) {
return cleanText![pos];
}
value = pos !== undefined ? options.text[pos] : options.text.join('\n');
} else {
value = options.text ?? '';
}
return value;
},
getVariable(name) {
const varValue = options.variables && options.variables[name];
return varValue != null ? varValue : name;
}
})
};
if (options.text != null && !textInserted) {
// Text given but no implicitly repeated elements: insert it into
// deepest child
const deepest = deepestNode(last(result.children));
if (deepest) {
const text = Array.isArray(options.text) ? options.text.join('\n') : options.text;
insertText(deepest, text);
if (deepest.name === 'a' && options.href) {
// Automatically update value of `<a>` element if inserting URL or email
insertHref(deepest, text);
}
}
}
return result;
}
/**
* Converts given statement to abbreviation nodes
*/
function convertStatement(node: TokenStatement, state: ConvertState): AbbreviationNode[] {
let result: AbbreviationNode[] = [];
if (node.repeat) {
// Node is repeated: we should create copies of given node
// and supply context token with actual repeater state
const original = node.repeat;
const repeat = { ...original } as Repeater;
repeat.count = repeat.implicit && Array.isArray(state.text)
? state.cleanText!.length
: (repeat.count || 1);
let items: AbbreviationNode[];
state.repeaters.push(repeat);
for (let i = 0; i < repeat.count; i++) {
repeat.value = i;
node.repeat = repeat;
items = isGroup(node)
? convertGroup(node, state)
: convertElement(node, state);
if (repeat.implicit && !state.inserted) {
// It’s an implicit repeater but no repeater placeholders found inside,
// we should insert text into deepest node
const target = last(items);
const deepest = target && deepestNode(target);
if (deepest) {
insertText(deepest, state.getText(repeat.value));
}
}
result = result.concat(items);
// We should output at least one repeated item even if it’s reached
// repeat limit
if (--state.repeatGuard <= 0) {
break;
}
}
state.repeaters.pop();
node.repeat = original;
if (repeat.implicit) {
state.inserted = true;
}
} else {
result = result.concat(isGroup(node) ? convertGroup(node, state) : convertElement(node, state));
}
return result;
}
function convertElement(node: TokenElement, state: ConvertState): AbbreviationNode[] {
let children: AbbreviationNode[] = [];
const elem = {
type: 'AbbreviationNode',
name: node.name && stringifyName(node.name, state),
value: node.value && stringifyValue(node.value, state),
attributes: void 0,
children,
repeat: node.repeat && { ...node.repeat },
selfClosing: node.selfClose,
} as AbbreviationNode;
let result: AbbreviationNode[] = [elem];
for (const child of node.elements) {
children = children.concat(convertStatement(child, state));
}
if (node.attributes) {
elem.attributes = [];
for (const attr of node.attributes) {
elem.attributes.push(convertAttribute(attr, state));
}
}
// In case if current node is a text-only snippet without fields, we should
// put all children as siblings
if (!elem.name && !elem.attributes && elem.value && !elem.value.some(isField)) {
// XXX it’s unclear that `children` is not bound to `elem`
// due to concat operation
result = result.concat(children);
} else {
elem.children = children;
}
return result;
}
function convertGroup(node: TokenGroup, state: ConvertState): AbbreviationNode[] {
let result: AbbreviationNode[] = [];
for (const child of node.elements) {
result = result.concat(convertStatement(child, state));
}
if (node.repeat) {
result = attachRepeater(result, node.repeat);
}
return result;
}
function convertAttribute(node: TokenAttribute, state: ConvertState): AbbreviationAttribute {
let implied = false;
let isBoolean = false;
let valueType: AttributeType = node.expression ? 'expression' : 'raw';
let value: Value[] | undefined;
const name = node.name && stringifyName(node.name, state);
if (name && name[0] === '!') {
implied = true;
}
if (name && name[name.length - 1] === '.') {
isBoolean = true;
}
if (node.value) {
const tokens = node.value.slice();
if (isQuote(tokens[0])) {
// It’s a quoted value: remove quotes from output but mark attribute
// value as quoted
const quote = tokens.shift() as Quote;
if (tokens.length && last(tokens).type === quote.type) {
tokens.pop();
}
valueType = quote.single ? 'singleQuote' : 'doubleQuote';
} else if (isBracket(tokens[0], 'expression', true)) {
// Value is expression: remove brackets but mark value type
valueType = 'expression';
tokens.shift();
if (isBracket(last(tokens), 'expression', false)) {
tokens.pop();
}
}
value = stringifyValue(tokens, state);
}
return {
name: isBoolean || implied
? name!.slice(implied ? 1 : 0, isBoolean ? -1 : void 0)
: name,
value,
boolean: isBoolean,
implied,
valueType,
multiple: node.multiple
};
}
/**
* Converts given token list to string
*/
function stringifyName(tokens: ValueToken[], state: ConvertState): string {
let str = '';
for (let i = 0; i < tokens.length; i++) {
str += stringify(tokens[i], state);
}
return str;
}
/**
* Converts given token list to value list
*/
function stringifyValue(tokens: ValueToken[], state: ConvertState): Value[] {
const result: Value[] = [];
let str = '';
for (let i = 0, token: ValueToken; i < tokens.length; i++) {
token = tokens[i];
if (isField(token)) {
// We should keep original fields in output since some editors has their
// own syntax for field or doesn’t support fields at all so we should
// capture actual field location in output stream
if (str) {
result.push(str);
str = '';
}
result.push(token);
} else {
str += stringify(token, state);
}
}
if (str) {
result.push(str);
}
return result;
}
export function isGroup(node: any): node is TokenGroup {
return node.type === 'TokenGroup';
}
function isField(token: any): token is Field {
return typeof token === 'object' && token.type === 'Field' && token.index != null;
}
function last<T>(arr: T[]): T {
return arr[arr.length - 1];
}
function deepestNode(node: AbbreviationNode): AbbreviationNode {
return node.children.length ? deepestNode(last(node.children)) : node;
}
function insertText(node: AbbreviationNode, text: string) {
if (node.value) {
const lastToken = last(node.value);
if (typeof lastToken === 'string') {
node.value[node.value.length - 1] += text;
} else {
node.value.push(text);
}
} else {
node.value = [text];
}
}
function insertHref(node: AbbreviationNode, text: string) {
let href = '';
if (urlRegex.test(text)) {
href = text;
if (!/\w+:/.test(href) && !href.startsWith('//')) {
href = `http://${href}`;
}
} else if (emailRegex.test(text)) {
href = `mailto:${text}`;
}
const hrefAttribute = node.attributes?.find(attr => attr.name === 'href');
if (!hrefAttribute) {
if (!node.attributes) {
node.attributes = [];
}
node.attributes.push({ name: 'href', value: [href], valueType: 'doubleQuote' });
} else if (!hrefAttribute.value) {
hrefAttribute.value = [href];
}
}
function attachRepeater(items: AbbreviationNode[], repeater: Repeater): AbbreviationNode[] {
for (const item of items) {
if (!item.repeat) {
item.repeat = { ...repeater };
}
}
return items;
}
================================================
FILE: packages/abbreviation/src/index.ts
================================================
import { ScannerError } from '@emmetio/scanner';
import parse, { type TokenGroup } from './parser';
import tokenize, { getToken, type AllTokens } from './tokenizer';
import convert from './convert';
import type { ParserOptions } from './types';
export { parse, tokenize, getToken, convert };
export * from './tokenizer/tokens';
export * from './types';
export type MarkupAbbreviation = TokenGroup;
/**
* Parses given abbreviation into node tree
*/
export default function parseAbbreviation(abbr: string | AllTokens[], options?: ParserOptions) {
try {
const tokens = typeof abbr === 'string' ? tokenize(abbr) : abbr;
return convert(parse(tokens, options), options);
} catch (err) {
if (err instanceof ScannerError && typeof abbr === 'string') {
err.message += `\n${abbr}\n${'-'.repeat(err.pos)}^`;
}
throw err;
}
}
================================================
FILE: packages/abbreviation/src/parser/TokenScanner.ts
================================================
import type { AllTokens } from '../tokenizer';
export interface TokenScanner {
tokens: AllTokens[];
start: number;
pos: number;
size: number;
}
type TestFn = (token?: AllTokens) => boolean;
export default function tokenScanner(tokens: AllTokens[]): TokenScanner {
return {
tokens,
start: 0,
pos: 0,
size: tokens.length
};
}
export function peek(scanner: TokenScanner): AllTokens | undefined {
return scanner.tokens[scanner.pos];
}
export function next(scanner: TokenScanner): AllTokens | undefined {
return scanner.tokens[scanner.pos++];
}
export function slice(scanner: TokenScanner, from = scanner.start, to = scanner.pos): AllTokens[] {
return scanner.tokens.slice(from, to);
}
export function readable(scanner: TokenScanner): boolean {
return scanner.pos < scanner.size;
}
export function consume(scanner: TokenScanner, test: TestFn): boolean {
const token = peek(scanner);
if (token && test(token)) {
scanner.pos++;
return true;
}
return false;
}
export function error(scanner: TokenScanner, message: string, token = peek(scanner)) {
if (token && token.start != null) {
message += ` at ${token.start}`;
}
const err = new Error(message);
err['pos'] = token && token.start;
return err;
}
export function consumeWhile(scanner: TokenScanner, test: TestFn): boolean {
const start = scanner.pos;
while (consume(scanner, test)) { /* */ }
return scanner.pos !== start;
}
================================================
FILE: packages/abbreviation/src/parser/index.ts
================================================
import type { NameToken, ValueToken, Repeater, AllTokens, BracketType, Bracket, Operator, OperatorType, Quote, WhiteSpace, Literal } from '../tokenizer';
import tokenScanner, { type TokenScanner, peek, consume, readable, next, error, slice } from './TokenScanner';
import type { ParserOptions } from '../types';
export type TokenStatement = TokenElement | TokenGroup;
export interface TokenAttribute {
name?: ValueToken[];
value?: ValueToken[];
expression?: boolean;
/**
* Indicates that current attribute was repeated multiple times in a row.
* Used to alter output of multiple shorthand attributes like `..` (double class)
*/
multiple?: boolean;
}
export interface TokenElement {
type: 'TokenElement';
name?: NameToken[];
attributes?: TokenAttribute[];
value?: ValueToken[];
repeat?: Repeater;
selfClose: boolean;
elements: TokenStatement[];
}
export interface TokenGroup {
type: 'TokenGroup';
elements: TokenStatement[];
repeat?: Repeater;
}
export default function abbreviation(abbr: AllTokens[], options: ParserOptions = {}): TokenGroup {
const scanner = tokenScanner(abbr);
const result = statements(scanner, options);
if (readable(scanner)) {
throw error(scanner, 'Unexpected character');
}
return result;
}
function statements(scanner: TokenScanner, options: ParserOptions): TokenGroup {
const result: TokenGroup = {
type: 'TokenGroup',
elements: []
};
let ctx: TokenStatement = result;
let node: TokenStatement | undefined;
const stack: TokenStatement[] = [];
while (readable(scanner)) {
if (node = element(scanner, options) || group(scanner, options)) {
ctx.elements.push(node);
if (consume(scanner, isChildOperator)) {
stack.push(ctx);
ctx = node;
} else if (consume(scanner, isSiblingOperator)) {
continue;
} else if (consume(scanner, isClimbOperator)) {
do {
if (stack.length) {
ctx = stack.pop()!;
}
} while (consume(scanner, isClimbOperator));
}
} else {
break;
}
}
return result;
}
/**
* Consumes group from given scanner
*/
function group(scanner: TokenScanner, options: ParserOptions): TokenGroup | undefined {
if (consume(scanner, isGroupStart)) {
const result = statements(scanner, options);
const token = next(scanner);
if (isBracket(token, 'group', false)) {
result.repeat = repeater(scanner);
}
return result;
}
}
/**
* Consumes single element from given scanner
*/
function element(scanner: TokenScanner, options: ParserOptions): TokenElement | undefined {
let attr: TokenAttribute | TokenAttribute[] | undefined;
const elem: TokenElement = {
type: 'TokenElement',
name: void 0,
attributes: void 0,
value: void 0,
repeat: void 0,
selfClose: false,
elements: []
};
if (elementName(scanner, options)) {
elem.name = slice(scanner) as NameToken[];
}
while (readable(scanner)) {
scanner.start = scanner.pos;
if (!elem.repeat && !isEmpty(elem) && consume(scanner, isRepeater)) {
elem.repeat = scanner.tokens[scanner.pos - 1] as Repeater;
} else if (!elem.value && text(scanner)) {
elem.value = getText(scanner);
} else if (attr = shortAttribute(scanner, 'id', options) || shortAttribute(scanner, 'class', options) || attributeSet(scanner)) {
if (!elem.attributes) {
elem.attributes = Array.isArray(attr) ? attr.slice() : [attr];
} else {
elem.attributes = elem.attributes.concat(attr);
}
} else {
if (!isEmpty(elem) && consume(scanner, isCloseOperator)) {
elem.selfClose = true;
if (!elem.repeat && consume(scanner, isRepeater)) {
elem.repeat = scanner.tokens[scanner.pos - 1] as Repeater;
}
}
break;
}
}
return !isEmpty(elem) ? elem : void 0;
}
/**
* Consumes attribute set from given scanner
*/
function attributeSet(scanner: TokenScanner): TokenAttribute[] | undefined {
if (consume(scanner, isAttributeSetStart)) {
const attributes: TokenAttribute[] = [];
let attr: TokenAttribute | undefined;
while (readable(scanner)) {
if (attr = attribute(scanner)) {
attributes.push(attr);
} else if (consume(scanner, isAttributeSetEnd)) {
break;
} else if (!consume(scanner, isWhiteSpace)) {
throw error(scanner, `Unexpected "${peek(scanner)!.type}" token`);
}
}
return attributes;
}
}
/**
* Consumes attribute shorthand (class or id) from given scanner
*/
function shortAttribute(scanner: TokenScanner, type: 'class' | 'id', options: ParserOptions): TokenAttribute | undefined {
if (isOperator(peek(scanner), type)) {
scanner.pos++;
// Consume multiple operators
let count = 1;
while (isOperator(peek(scanner), type)) {
scanner.pos++;
count++;
}
const attr: TokenAttribute = {
name: [createLiteral(type)]
};
if (count > 1) {
attr.multiple = true;
}
// Consume expression after shorthand start for React-like components
if (options.jsx && text(scanner)) {
attr.value = getText(scanner);
attr.expression = true;
} else if (quoted(scanner)) {
attr.value = slice(scanner, scanner.start + 1, scanner.pos - 1) as ValueToken[];
} else {
attr.value = literal(scanner) ? slice(scanner) as ValueToken[] : void 0;
}
return attr;
}
}
/**
* Consumes single attribute from given scanner
*/
function attribute(scanner: TokenScanner): TokenAttribute | undefined {
if (quoted(scanner)) {
// Consumed quoted value: it’s a value for default attribute
return {
value: slice(scanner) as ValueToken[]
};
}
if (literal(scanner, true)) {
const name = slice(scanner) as NameToken[];
let value: ValueToken[] | undefined;
if (consume(scanner, isEquals)) {
if (quoted(scanner) || literal(scanner, true)) {
value = slice(scanner) as ValueToken[];
}
}
return { name, value };
}
}
function repeater(scanner: TokenScanner): Repeater | undefined {
return isRepeater(peek(scanner))
? scanner.tokens[scanner.pos++] as Repeater
: void 0;
}
/**
* Consumes quoted value from given scanner, if possible
*/
function quoted(scanner: TokenScanner): boolean {
const start = scanner.pos;
const quote = peek(scanner);
if (isQuote(quote)) {
scanner.pos++;
while (readable(scanner)) {
if (isQuote(next(scanner), quote.single)) {
scanner.start = start;
return true;
}
}
throw error(scanner, 'Unclosed quote', quote);
}
return false;
}
/**
* Consumes literal (unquoted value) from given scanner
*/
function literal(scanner: TokenScanner, allowBrackets?: boolean): boolean {
const start = scanner.pos;
const brackets: { [type in BracketType]: number } = {
attribute: 0,
expression: 0,
group: 0
};
while (readable(scanner)) {
const token = peek(scanner);
if (brackets.expression) {
// If we’re inside expression, we should consume all content in it
if (isBracket(token, 'expression')) {
brackets[token.context] += token.open ? 1 : -1;
}
} else if (isQuote(token) || isOperator(token) || isWhiteSpace(token) || isRepeater(token)) {
break;
} else if (isBracket(token)) {
if (!allowBrackets) {
break;
}
if (token.open) {
brackets[token.context]++;
} else if (!brackets[token.context]) {
// Stop if found unmatched closing brace: it must be handled
// by parent consumer
break;
} else {
brackets[token.context]--;
}
}
scanner.pos++;
}
if (start !== scanner.pos) {
scanner.start = start;
return true;
}
return false;
}
/**
* Consumes element name from given scanner
*/
function elementName(scanner: TokenScanner, options: ParserOptions): boolean {
const start = scanner.pos;
if (options.jsx && consume(scanner, isCapitalizedLiteral)) {
// Check for edge case: consume immediate capitalized class names
// for React-like components, e.g. `Foo.Bar.Baz`
while (readable(scanner)) {
const { pos } = scanner;
if (!consume(scanner, isClassNameOperator) || !consume(scanner, isCapitalizedLiteral)) {
scanner.pos = pos;
break;
}
}
}
while (readable(scanner) && consume(scanner, isElementName)) {
// empty
}
if (scanner.pos !== start) {
scanner.start = start;
return true;
}
return false;
}
/**
* Consumes text value from given scanner
*/
function text(scanner: TokenScanner): boolean {
const start = scanner.pos;
if (consume(scanner, isTextStart)) {
let brackets = 0;
while (readable(scanner)) {
const token = next(scanner);
if (isBracket(token, 'expression')) {
if (token.open) {
brackets++;
} else if (!brackets) {
break;
} else {
brackets--;
}
}
}
scanner.start = start;
return true;
}
return false;
}
function getText(scanner: TokenScanner): ValueToken[] {
let from = scanner.start;
let to = scanner.pos;
if (isBracket(scanner.tokens[from], 'expression', true)) {
from++;
}
if (isBracket(scanner.tokens[to - 1], 'expression', false)) {
to--;
}
return slice(scanner, from, to) as ValueToken[];
}
export function isBracket(token: AllTokens | undefined, context?: BracketType, isOpen?: boolean): token is Bracket {
return Boolean(token && token.type === 'Bracket'
&& (!context || token.context === context)
&& (isOpen == null || token.open === isOpen));
}
export function isOperator(token: AllTokens | undefined, type?: OperatorType): token is Operator {
return Boolean(token && token.type === 'Operator' && (!type || token.operator === type));
}
export function isQuote(token: AllTokens | undefined, isSingle?: boolean): token is Quote {
return Boolean(token && token.type === 'Quote' && (isSingle == null || token.single === isSingle));
}
function isWhiteSpace(token?: AllTokens): token is WhiteSpace {
return Boolean(token && token.type === 'WhiteSpace');
}
function isEquals(token: AllTokens) {
return isOperator(token, 'equal');
}
function isRepeater(token?: AllTokens): token is Repeater {
return Boolean(token && token.type === 'Repeater');
}
function isLiteral(token: AllTokens): token is Literal {
return token.type === 'Literal';
}
function isCapitalizedLiteral(token: AllTokens) {
if (isLiteral(token)) {
const ch = token.value.charCodeAt(0);
return ch >= 65 && ch <= 90;
}
return false;
}
function isElementName(token: AllTokens): boolean {
return token.type === 'Literal' || token.type === 'RepeaterNumber' || token.type === 'RepeaterPlaceholder';
}
function isClassNameOperator(token: AllTokens) {
return isOperator(token, 'class');
}
function isAttributeSetStart(token?: AllTokens) {
return isBracket(token, 'attribute', true);
}
function isAttributeSetEnd(token?: AllTokens) {
return isBracket(token, 'attribute', false);
}
function isTextStart(token: AllTokens) {
return isBracket(token, 'expression', true);
}
function isGroupStart(token: AllTokens) {
return isBracket(token, 'group', true);
}
function createLiteral(value: string): Literal {
return { type: 'Literal', value };
}
function isEmpty(elem: TokenElement): boolean {
return !elem.name && !elem.value && !elem.attributes;
}
function isChildOperator(token: AllTokens) {
return isOperator(token, 'child');
}
function isSiblingOperator(token: AllTokens) {
return isOperator(token, 'sibling');
}
function isClimbOperator(token: AllTokens) {
return isOperator(token, 'climb');
}
function isCloseOperator(token: AllTokens) {
return isOperator(token, 'close');
}
================================================
FILE: packages/abbreviation/src/stringify.ts
================================================
import type { Token, Literal, Bracket, Field, RepeaterPlaceholder, Repeater, RepeaterNumber, ValueToken, Quote, Operator, OperatorType, WhiteSpace } from './tokenizer/tokens';
import type { ConvertState } from './types';
type TokenVisitor = (token: Token, state: ConvertState) => string;
const operators: { [key in OperatorType]: string } = {
child: '>',
class: '.',
climb: '^',
id: '#',
equal: '=',
close: '/',
sibling: '+'
};
const tokenVisitor: { [name: string]: TokenVisitor } = {
Literal(token: Literal): string {
return token.value;
},
Quote(token: Quote) {
return token.single ? '\'' : '"';
},
Bracket(token: Bracket): string {
if (token.context === 'attribute') {
return token.open ? '[' : ']';
} else if (token.context === 'expression') {
return token.open ? '{' : '}';
} else {
return token.open ? '(' : '}';
}
},
Operator(token: Operator) {
return operators[token.operator];
},
Field(token: Field, state) {
if (token.index != null) {
// It’s a field: by default, return TextMate-compatible field
return token.name
? `\${${token.index}:${token.name}}`
: `\${${token.index}`;
} else if (token.name) {
// It’s a variable
return state.getVariable(token.name);
}
return '';
},
RepeaterPlaceholder(token: RepeaterPlaceholder, state) {
// Find closest implicit repeater
let repeater: Repeater | undefined;
for (let i = state.repeaters.length - 1; i >= 0; i--) {
if (state.repeaters[i]!.implicit) {
repeater = state.repeaters[i]!;
break;
}
}
state.inserted = true;
return state.getText(repeater && repeater.value);
},
RepeaterNumber(token: RepeaterNumber, state) {
let value = 1;
const lastIx = state.repeaters.length - 1;
// const repeaterIx = Math.max(0, state.repeaters.length - 1 - token.parent);
const repeater = state.repeaters[lastIx];
if (repeater) {
value = token.reverse
? token.base + repeater.count - repeater.value! - 1
: token.base + repeater.value!;
if (token.parent) {
const parentIx = Math.max(0, lastIx - token.parent);
if (parentIx !== lastIx) {
const parentRepeater = state.repeaters[parentIx];
value += repeater.count * parentRepeater.value;
}
}
}
let result = String(value);
while (result.length < token.size) {
result = '0' + result;
}
return result;
},
WhiteSpace(token: WhiteSpace) {
return token.value;
}
};
/**
* Converts given value token to string
*/
export default function stringify(token: ValueToken, state: ConvertState): string {
if (!tokenVisitor[token.type]) {
throw new Error(`Unknown token ${token.type}`);
}
return tokenVisitor[token.type](token, state);
}
================================================
FILE: packages/abbreviation/src/tokenizer/index.ts
================================================
import Scanner, { isSpace, isQuote, isNumber, isAlpha, isAlphaNumericWord, isUmlaut } from '@emmetio/scanner';
import type { Literal, WhiteSpace, Quote, Bracket, BracketType, OperatorType, Operator, RepeaterPlaceholder, Repeater, Field, RepeaterNumber, AllTokens } from './tokens';
import { Chars, escaped } from './utils';
export * from './tokens';
type Context = { [ctx in BracketType]: number } & { quote: number };
export default function tokenize(source: string): AllTokens[] {
const scanner = new Scanner(source);
const result: AllTokens[] = [];
const ctx: Context = {
group: 0,
attribute: 0,
expression: 0,
quote: 0
};
let ch = 0;
let token: AllTokens | undefined;
while (!scanner.eof()) {
ch = scanner.peek();
token = getToken(scanner, ctx);
if (token) {
result.push(token);
if (token.type === 'Quote') {
ctx.quote = ch === ctx.quote ? 0 : ch;
} else if (token.type === 'Bracket') {
ctx[token.context] += token.open ? 1 : -1;
}
} else {
throw scanner.error('Unexpected character');
}
}
return result;
}
/**
* Returns next token from given scanner, if possible
*/
export function getToken(scanner: Scanner, ctx: Context): AllTokens | undefined {
return field(scanner, ctx)
|| repeaterPlaceholder(scanner)
|| repeaterNumber(scanner)
|| repeater(scanner)
|| whiteSpace(scanner)
|| literal(scanner, ctx)
|| operator(scanner)
|| quote(scanner)
|| bracket(scanner);
}
/**
* Consumes literal from given scanner
*/
function literal(scanner: Scanner, ctx: Context): Literal | undefined {
const start = scanner.pos;
const expressionStart = ctx.expression;
let value = '';
while (!scanner.eof()) {
// Consume escaped sequence no matter of context
if (escaped(scanner)) {
value += scanner.current();
continue;
}
const ch = scanner.peek();
if (ch === Chars.Slash && !ctx.quote && !ctx.expression && !ctx.attribute) {
// Special case for `/` character between numbers in class names
const prev = scanner.string.charCodeAt(scanner.pos - 1);
const next = scanner.string.charCodeAt(scanner.pos + 1);
if (isNumber(prev) && isNumber(next)) {
value += scanner.string[scanner.pos++];
continue;
}
}
if (ch === ctx.quote || ch === Chars.Dollar || isAllowedOperator(ch, ctx)) {
// 1. Found matching quote
// 2. The `$` character has special meaning in every context
// 3. Depending on context, some characters should be treated as operators
break;
}
if (expressionStart) {
// Consume nested expressions, e.g. span{{foo}}
if (ch === Chars.CurlyBracketOpen) {
ctx.expression++;
} else if (ch === Chars.CurlyBracketClose) {
if (ctx.expression > expressionStart) {
ctx.expression--;
} else {
break;
}
}
} else if (!ctx.quote) {
// Consuming element name
if (!ctx.attribute && !isElementName(ch)) {
break;
}
if (isAllowedSpace(ch, ctx) || isAllowedRepeater(ch, ctx) || isQuote(ch) || bracketType(ch)) {
// Stop for characters not allowed in unquoted literal
break;
}
}
value += scanner.string[scanner.pos++];
}
if (start !== scanner.pos) {
scanner.start = start;
return {
type: 'Literal',
value,
start,
end: scanner.pos
};
}
}
/**
* Consumes white space characters as string literal from given scanner
*/
function whiteSpace(scanner: Scanner): WhiteSpace | undefined {
const start = scanner.pos;
if (scanner.eatWhile(isSpace)) {
return {
type: 'WhiteSpace',
start,
end: scanner.pos,
value: scanner.substring(start, scanner.pos)
};
}
}
/**
* Consumes quote from given scanner
*/
function quote(scanner: Scanner): Quote | undefined {
const ch = scanner.peek();
if (isQuote(ch)) {
return {
type: 'Quote',
single: ch === Chars.SingleQuote,
start: scanner.pos++,
end: scanner.pos
};
}
}
/**
* Consumes bracket from given scanner
*/
function bracket(scanner: Scanner): Bracket | undefined {
const ch = scanner.peek();
const context = bracketType(ch);
if (context) {
return {
type: 'Bracket',
open: isOpenBracket(ch),
context,
start: scanner.pos++,
end: scanner.pos
};
}
}
/**
* Consumes operator from given scanner
*/
function operator(scanner: Scanner): Operator | undefined {
const op = operatorType(scanner.peek());
if (op) {
return {
type: 'Operator',
operator: op,
start: scanner.pos++,
end: scanner.pos
};
}
}
/**
* Consumes node repeat token from current stream position and returns its
* parsed value
*/
function repeater(scanner: Scanner): Repeater | undefined {
const start = scanner.pos;
if (scanner.eat(Chars.Asterisk)) {
scanner.start = scanner.pos;
let count = 1;
let implicit = false;
if (scanner.eatWhile(isNumber)) {
count = Number(scanner.current());
} else {
implicit = true;
}
return {
type: 'Repeater',
count,
value: 0,
implicit,
start,
end: scanner.pos
};
}
}
/**
* Consumes repeater placeholder `$#` from given scanner
*/
function repeaterPlaceholder(scanner: Scanner): RepeaterPlaceholder | undefined {
const start = scanner.pos;
if (scanner.eat(Chars.Dollar) && scanner.eat(Chars.Hash)) {
return {
type: 'RepeaterPlaceholder',
value: void 0,
start,
end: scanner.pos
};
}
scanner.pos = start;
}
/**
* Consumes numbering token like `$` from given scanner state
*/
function repeaterNumber(scanner: Scanner): RepeaterNumber | undefined {
const start = scanner.pos;
if (scanner.eatWhile(Chars.Dollar)) {
const size = scanner.pos - start;
let reverse = false;
let base = 1;
let parent = 0;
if (scanner.eat(Chars.At)) {
// Consume numbering modifiers
while (scanner.eat(Chars.Climb)) {
parent++;
}
reverse = scanner.eat(Chars.Dash);
scanner.start = scanner.pos;
if (scanner.eatWhile(isNumber)) {
base = Number(scanner.current());
}
}
scanner.start = start;
return {
type: 'RepeaterNumber',
size,
reverse,
base,
parent,
start,
end: scanner.pos
};
}
}
function field(scanner: Scanner, ctx: Context): Field | undefined {
const start = scanner.pos;
// Fields are allowed inside expressions and attributes
if ((ctx.expression || ctx.attribute) && scanner.eat(Chars.Dollar) && scanner.eat(Chars.CurlyBracketOpen)) {
scanner.start = scanner.pos;
let index: number | undefined;
let name: string = '';
if (scanner.eatWhile(isNumber)) {
// It’s a field
index = Number(scanner.current());
name = scanner.eat(Chars.Colon) ? consumePlaceholder(scanner) : '';
} else if (isAlpha(scanner.peek())) {
// It’s a variable
name = consumePlaceholder(scanner);
}
if (scanner.eat(Chars.CurlyBracketClose)) {
return {
type: 'Field',
index, name,
start,
end: scanner.pos
};
}
throw scanner.error('Expecting }');
}
// If we reached here then there’s no valid field here, revert
// back to starting position
scanner.pos = start;
}
/**
* Consumes a placeholder: value right after `:` in field. Could be empty
*/
function consumePlaceholder(stream: Scanner): string {
const stack: number[] = [];
stream.start = stream.pos;
while (!stream.eof()) {
if (stream.eat(Chars.CurlyBracketOpen)) {
stack.push(stream.pos);
} else if (stream.eat(Chars.CurlyBracketClose)) {
if (!stack.length) {
stream.pos--;
break;
}
stack.pop();
} else {
stream.pos++;
}
}
if (stack.length) {
stream.pos = stack.pop()!;
throw stream.error(`Expecting }`);
}
return stream.current();
}
/**
* Check if given character code is an operator and it’s allowed in current context
*/
function isAllowedOperator(ch: number, ctx: Context): boolean {
const op = operatorType(ch);
if (!op || ctx.quote || ctx.expression) {
// No operators inside quoted values or expressions
return false;
}
// Inside attributes, only `equals` is allowed
return !ctx.attribute || op === 'equal';
}
/**
* Check if given character is a space character and is allowed to be consumed
* as a space token in current context
*/
function isAllowedSpace(ch: number, ctx: Context): boolean {
return isSpace(ch) && !ctx.expression;
}
/**
* Check if given character can be consumed as repeater in current context
*/
function isAllowedRepeater(ch: number, ctx: Context): boolean {
return ch === Chars.Asterisk && !ctx.attribute && !ctx.expression;
}
/**
* If given character is a bracket, returns it’s type
*/
function bracketType(ch: number): BracketType | undefined {
if (ch === Chars.RoundBracketOpen || ch === Chars.RoundBracketClose) {
return 'group';
}
if (ch === Chars.SquareBracketOpen || ch === Chars.SquareBracketClose) {
return 'attribute';
}
if (ch === Chars.CurlyBracketOpen || ch === Chars.CurlyBracketClose) {
return 'expression';
}
}
/**
* If given character is an operator, returns it’s type
*/
function operatorType(ch: number): OperatorType | undefined {
return (ch === Chars.Child && 'child')
|| (ch === Chars.Sibling && 'sibling')
|| (ch === Chars.Climb && 'climb')
|| (ch === Chars.Dot && 'class')
|| (ch === Chars.Hash && 'id')
|| (ch === Chars.Slash && 'close')
|| (ch === Chars.Equals && 'equal')
|| void 0;
}
/**
* Check if given character is an open bracket
*/
function isOpenBracket(ch: number): boolean {
return ch === Chars.CurlyBracketOpen
|| ch === Chars.SquareBracketOpen
|| ch === Chars.RoundBracketOpen;
}
/**
* Check if given character is allowed in element name
*/
function isElementName(ch: number) {
return isAlphaNumericWord(ch)
|| isUmlaut(ch)
|| ch === Chars.Dash
|| ch === Chars.Colon
|| ch === Chars.Excl;
}
================================================
FILE: packages/abbreviation/src/tokenizer/tokens.ts
================================================
export type OperatorType = 'child' | 'sibling' | 'climb' | 'class' | 'id' | 'close' | 'equal';
export type BracketType = 'group' | 'attribute' | 'expression';
export type AllTokens =
Bracket | Field | Literal | Operator | Quote | Repeater | RepeaterNumber |
RepeaterPlaceholder | WhiteSpace;
export type NameToken = Literal | RepeaterNumber;
export type ValueToken = Literal | Quote | Bracket | Field | RepeaterPlaceholder | RepeaterNumber;
export interface Token {
type: string;
/** Location of token start in source */
start?: number;
/** Location of token end in source */
end?: number;
}
export interface Repeater extends Token {
type: 'Repeater';
/** How many times context element should be repeated */
count: number;
/** Position of context element in its repeating sequence */
value: number;
/** Repeater is implicit, e.g. repeated by the amount of text lines selected by user */
implicit: boolean;
}
export interface RepeaterNumber extends Token {
type: 'RepeaterNumber';
/** Size of repeater content, e.g. the amount consequent numbering characters */
size: number;
/** Should output numbering in reverse order? */
reverse: boolean;
/** Base value to start numbering from */
base: number;
/** Parent offset from which numbering should be used */
parent: number;
}
export interface RepeaterPlaceholder extends Token {
type: 'RepeaterPlaceholder';
/** Value to insert instead of placeholder */
value?: string;
}
export interface Field extends Token {
type: 'Field';
index?: number;
name: string;
}
export interface Operator extends Token {
type: 'Operator';
operator: OperatorType;
}
export interface Bracket extends Token {
type: 'Bracket';
open: boolean;
context: BracketType;
}
export interface Quote extends Token {
type: 'Quote';
single: boolean;
}
export interface Literal extends Token {
type: 'Literal';
value: string;
}
export interface WhiteSpace extends Token {
type: 'WhiteSpace';
value: string;
}
================================================
FILE: packages/abbreviation/src/tokenizer/utils.ts
================================================
import type Scanner from '@emmetio/scanner';
export const enum Chars {
/** `{` character */
CurlyBracketOpen = 123,
/** `}` character */
CurlyBracketClose = 125,
/** `\\` character */
Escape = 92,
/** `=` character */
Equals = 61,
/** `[` character */
SquareBracketOpen = 91,
/** `]` character */
SquareBracketClose = 93,
/** `*` character */
Asterisk = 42,
/** `#` character */
Hash = 35,
/** `$` character */
Dollar = 36,
/** `-` character */
Dash = 45,
/** `.` character */
Dot = 46,
/** `/` character */
Slash = 47,
/** `:` character */
Colon = 58,
/** `!` character */
Excl = 33,
/** `@` character */
At = 64,
/** `_` character */
Underscore = 95,
/** `(` character */
RoundBracketOpen = 40,
/** `)` character */
RoundBracketClose = 41,
/** `+` character */
Sibling = 43,
/** `>` character */
Child = 62,
/** `^` character */
Climb = 94,
/** `'` character */
SingleQuote = 39,
/** `""` character */
DoubleQuote = 34,
}
/**
* If consumes escape character, sets current stream range to escaped value
*/
export function escaped(scanner: Scanner): boolean {
if (scanner.eat(Chars.Escape)) {
scanner.start = scanner.pos;
if (!scanner.eof()) {
scanner.pos++;
}
return true;
}
return false;
}
================================================
FILE: packages/abbreviation/src/types.ts
================================================
import type { Field, Repeater } from './tokenizer';
export interface ParserOptions {
/** Text strings to insert into implicitly repeated elements */
text?: string | string[];
/** Variable values for `${var}` tokens */
variables?: { [name: string]: string };
/** Max amount of repeated elements in abbreviation */
maxRepeat?: number;
/** Enabled JSX parsing mode */
jsx?: boolean;
/** Enable inserting text into href attribute of links */
href?: boolean;
}
export interface ConvertState {
inserted: boolean;
text?: string | string[];
cleanText?: string | string[];
repeatGuard: number;
/** Context repeaters, e.g. all actual repeaters from parent */
repeaters: Repeater[];
getText(pos?: number): string;
getVariable(name: string): string;
}
export type Value = string | Field;
export type AttributeType = 'raw' | 'singleQuote' | 'doubleQuote' | 'expression';
export interface Abbreviation {
type: 'Abbreviation';
children: AbbreviationNode[];
}
export interface AbbreviationNode {
type: 'AbbreviationNode';
name?: string;
value?: Value[];
repeat?: Repeater;
attributes?: AbbreviationAttribute[];
children: AbbreviationNode[];
/** Indicates current element is self-closing, e.g. should not contain closing pair */
selfClosing?: boolean;
}
export interface AbbreviationAttribute {
name?: string;
value?: Value[];
/** Indicates type of value stored in `.value` property */
valueType: AttributeType;
/** Attribute is boolean (e.g.name equals value) */
boolean?: boolean;
/** Attribute is implied (e.g.must be outputted only if contains non-null value) */
implied?: boolean;
/**
* Internal property that indicates that given attribute was specified
* more than once as a shorthand. E.g. `..` is a multiple `class` attribute
*/
multiple?: boolean;
}
================================================
FILE: packages/abbreviation/test/assets/stringify-node.ts
================================================
import type { Abbreviation, AbbreviationNode, Value, AbbreviationAttribute } from '../../src';
export default function stringify(abbr: Abbreviation): string {
return abbr.children.map(elem).join('');
}
function elem(node: AbbreviationNode): string {
const name = node.name || '?';
const attributes = node.attributes
? node.attributes.map(attr => ' ' + attribute(attr))
: '';
const value = node.value ? stringifyValue(node.value) : '';
const repeat = node.repeat ? `*${node.repeat.count}@${node.repeat.value}` : '';
return node.selfClosing && !node.value && !node.children.length
? `<${name}${repeat}${attributes} />`
: `<${name}${repeat}${attributes}>${value}${node.children.map(elem).join('')}</${name}>`;
}
function attribute(attr: AbbreviationAttribute): string {
const name = attr.name || '?';
const value = attr.value ? `"${stringifyValue(attr.value)}"` : null;
return value != null ? `${name}=${value}` : name;
}
function stringifyValue(items: Value[]): string {
return items.map(item =>
typeof item === 'string'
? item
: (item.name ? `\${${item.index!}:${item.name}}` : `\${${item.index!}}`)).join('');
}
================================================
FILE: packages/abbreviation/test/assets/stringify.ts
================================================
import type { AllTokens, Repeater, RepeaterNumber, Field, OperatorType, Operator, Bracket, Quote, Literal } from '../../src/tokenizer';
import type { TokenElement, TokenAttribute, TokenGroup, TokenStatement } from '../../src/parser';
type TokenVisitor = <T extends AllTokens>(token: T) => string;
interface TokenVisitorMap {
[nodeType: string]: TokenVisitor;
}
const operatorMap: { [name in OperatorType]: string } = {
id: '#',
class: '.',
equal: '=',
child: '>',
climb: '^',
sibling: '+',
close: '/'
};
const tokenVisitors = {
Repeater(token: Repeater) {
return `*${token.implicit ? '' : token.count}`;
},
RepeaterNumber(token: RepeaterNumber) {
return '$'.repeat(token.size);
},
RepeaterPlaceholder() {
return '$#';
},
Field(node: Field) {
const index = node.index != null ? String(node.index) : '';
const sep = index && node.name ? ':' : '';
return `\${${index}${sep}${node.name}}`;
},
Operator(node: Operator) {
return operatorMap[node.operator];
},
Bracket(node: Bracket) {
if (node.context === 'attribute') {
return node.open ? '[' : ']';
}
if (node.context === 'expression') {
return node.open ? '{' : '}';
}
if (node.context === 'group') {
return node.open ? '(' : ')';
}
return '?';
},
Quote(node: Quote) {
return node.single ? '\'' : '"';
},
Literal(node: Literal) {
return node.value;
},
WhiteSpace() {
return ' ';
}
} as TokenVisitorMap;
function statement(node: TokenElement | TokenGroup): string {
if (node.type === 'TokenGroup') {
return `(${content(node)})${node.repeat ? str(node.repeat) : ''}`;
}
return element(node);
}
function element(node: TokenElement): string {
const name = node.name ? tokenList(node.name) : '?';
const repeat = node.repeat ? str(node.repeat) : '';
const attributes = node.attributes ? node.attributes.map(attribute).join(' ') : '';
if (node.selfClose && !node.elements.length) {
return `<${name}${repeat}${attributes ? ' ' + attributes : ''} />`;
}
return `<${name}${repeat}${attributes ? ' ' + attributes : ''}>${tokenList(node.value)}${content(node)}</${name}>`;
}
function attribute(attr: TokenAttribute): string {
const name = tokenList(attr.name) || '?';
return attr.value ? `${name}=${tokenList(attr.value)}` : name;
}
function tokenList(tokens?: AllTokens[]): string {
return tokens ? tokens.map(str).join('') : '';
}
function str(token: AllTokens): string {
if (token.type in tokenVisitors) {
return tokenVisitors[token.type](token);
}
throw new Error(`Unknown token "${token.type}"`);
}
function content(node: TokenStatement): string {
return node.elements.map(statement).join('');
}
export default function stringify(abbr: TokenGroup): string {
return content(abbr);
}
================================================
FILE: packages/abbreviation/test/convert.ts
================================================
import { describe, it } from 'node:test';
import { equal } from 'node:assert';
import parser, { type ParserOptions } from '../src';
import stringify from './assets/stringify-node';
function parse(abbr: string, options?: ParserOptions) {
return stringify(parser(abbr, options));
}
describe('Convert token abbreviations', () => {
it('basic', () => {
equal(parse('input[value="text$"]*2'), '<input*2@0 value="text1"></input><input*2@1 value="text2"></input>');
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>');
equal(parse('ul>li.item$*', { text: ['foo$', 'bar$'] }), '<ul><li*2@0 class="item1">foo$</li><li*2@1 class="item2">bar$</li></ul>');
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>');
equal(parse('ul>li.item$*'), '<ul><li*1@0 class="item1"></li></ul>');
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>');
equal(parse('p{hi}', { text: ['hello'] }), '<p>hihello</p>');
equal(parse('p*{hi}', { text: ['1', '2'] }), '<p*2@0>hi1</p><p*2@1>hi2</p>');
equal(parse('div>p+p{hi}', { text: ['hello'] }), '<div><p></p><p>hihello</p></div>');
equal(parse('html[lang=${lang}]'), '<html lang="lang"></html>');
equal(parse('html.one.two'), '<html class="one", class="two"></html>');
equal(parse('html.one[two=three]'), '<html class="one", two="three"></html>');
equal(parse('div{[}+a{}'), '<div>[</div><a></a>');
});
it('unroll', () => {
equal(parse('a>(b>c)+d'), '<a><b><c></c></b><d></d></a>');
equal(parse('(a>b)+(c>d)'), '<a><b></b></a><c><d></d></c>');
equal(parse('a>((b>c)(d>e))f'), '<a><b><c></c></b><d><e></e></d><f></f></a>');
equal(parse('a>((((b>c))))+d'), '<a><b><c></c></b><d></d></a>');
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>');
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>');
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>');
equal(parse('a>(b+c)*2'), '<a><b*2@0></b><c*2@0></c><b*2@1></b><c*2@1></c></a>');
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>');
// Should move `<div>` as sibling of `{foo}`
equal(parse('p>{foo}>div'), '<p><?>foo</?><div></div></p>');
equal(parse('p>{foo ${0}}>div'), '<p><?>foo ${0}<div></div></?></p>');
});
it('limit unroll', () => {
// Limit amount of repeated elements
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>');
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>');
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>');
});
it('parent repeater', () => {
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>');
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>');
});
it('href', () => {
equal(parse('a', { href: true, text: 'https://www.google.it' }), '<a href="https://www.google.it">https://www.google.it</a>');
equal(parse('a', { href: true, text: 'www.google.it' }), '<a href="http://www.google.it">www.google.it</a>');
equal(parse('a', { href: true, text: 'google.it' }), '<a href="">google.it</a>');
equal(parse('a', { href: true, text: 'test here' }), '<a href="">test here</a>');
equal(parse('a', { href: true, text: 'test@domain.com' }), '<a href="mailto:test@domain.com">test@domain.com</a>');
equal(parse('a', { href: true, text: 'test here test@domain.com' }), '<a href="">test here test@domain.com</a>');
equal(parse('a', { href: true, text: 'test here www.domain.com' }), '<a href="">test here www.domain.com</a>');
equal(parse('a[href=]', { href: true, text: 'https://www.google.it' }), '<a href="https://www.google.it">https://www.google.it</a>');
equal(parse('a[href=]', { href: true, text: 'www.google.it' }), '<a href="http://www.google.it">www.google.it</a>');
equal(parse('a[href=]', { href: true, text: 'google.it' }), '<a href="">google.it</a>');
equal(parse('a[href=]', { href: true, text: 'test here' }), '<a href="">test here</a>');
equal(parse('a[href=]', { href: true, text: 'test@domain.com' }), '<a href="mailto:test@domain.com">test@domain.com</a>');
equal(parse('a[href=]', { href: true, text: 'test here test@domain.com' }), '<a href="">test here test@domain.com</a>');
equal(parse('a[href=]', { href: true, text: 'test here www.domain.com' }), '<a href="">test here www.domain.com</a>');
equal(parse('a[class=here]', { href: true, text: 'test@domain.com' }), '<a class="here", href="mailto:test@domain.com">test@domain.com</a>');
equal(parse('a.here', { href: true, text: 'www.domain.com' }), '<a class="here", href="http://www.domain.com">www.domain.com</a>');
equal(parse('a[class=here]', { href: true, text: 'test here test@domain.com' }), '<a class="here", href="">test here test@domain.com</a>');
equal(parse('a.here', { href: true, text: 'test here www.domain.com' }), '<a class="here", href="">test here www.domain.com</a>');
equal(parse('a[href="www.google.it"]', { href: false, text: 'test' }), '<a href="www.google.it">test</a>');
equal(parse('a[href="www.example.com"]', { href: true, text: 'www.google.it' }), '<a href="www.example.com">www.google.it</a>');
});
it('wrap basic', () => {
equal(parse('p', { text: 'test' }), '<p>test</p>');
equal(parse('p', { text: ['test'] }), '<p>test</p>');
equal(parse('p', { text: ['test1', 'test2'] }), '<p>test1\ntest2</p>');
equal(parse('p', { text: ['test1', '', 'test2'] }), '<p>test1\n\ntest2</p>');
equal(parse('p*', { text: ['test1', 'test2'] }), '<p*2@0>test1</p><p*2@1>test2</p>');
equal(parse('p*', { text: ['test1', '', 'test2'] }), '<p*2@0>test1</p><p*2@1>test2</p>');
})
});
================================================
FILE: packages/abbreviation/test/parser.ts
================================================
import { describe, it } from 'node:test';
import { strictEqual as equal, throws } from 'node:assert';
import parser from '../src/parser';
import tokenizer from '../src/tokenizer';
import stringify from './assets/stringify';
import type { ParserOptions } from '../src';
const parse = (abbr: string, options?: ParserOptions) => parser(tokenizer(abbr), options);
const str = (abbr: string, options?: ParserOptions) => stringify(parse(abbr, options));
describe('Parser', () => {
it('basic abbreviations', () => {
equal(str('p'), '<p></p>');
equal(str('p{text}'), '<p>text</p>');
equal(str('h$'), '<h$></h$>');
equal(str('.nav'), '<? class=nav></?>');
equal(str('div.width1\\/2'), '<div class=width1/2></div>');
equal(str('#sample*3'), '<?*3 id=sample></?>');
// ulmauts, https://github.com/emmetio/emmet/issues/439
equal(str('DatenSätze^'), '<DatenSätze></DatenSätze>')
// https://github.com/emmetio/emmet/issues/562
equal(str('li[repeat.for="todo of todoList"]'), '<li repeat.for="todo of todoList"></li>', 'Dots in attribute names');
equal(str('a>b'), '<a><b></b></a>');
equal(str('a+b'), '<a></a><b></b>');
equal(str('a+b>c+d'), '<a></a><b><c></c><d></d></b>');
equal(str('a>b>c+e'), '<a><b><c></c><e></e></b></a>');
equal(str('a>b>c^d'), '<a><b><c></c></b><d></d></a>');
equal(str('a>b>c^^^^d'), '<a><b><c></c></b></a><d></d>');
equal(str('a:b>c'), '<a:b><c></c></a:b>');
equal(str('ul.nav[title="foo"]'), '<ul class=nav title="foo"></ul>');
});
it('groups', () => {
equal(str('a>(b>c)+d'), '<a>(<b><c></c></b>)<d></d></a>');
equal(str('(a>b)+(c>d)'), '(<a><b></b></a>)(<c><d></d></c>)');
equal(str('a>((b>c)(d>e))f'), '<a>((<b><c></c></b>)(<d><e></e></d>))<f></f></a>');
equal(str('a>((((b>c))))+d'), '<a>((((<b><c></c></b>))))<d></d></a>');
equal(str('a>(((b>c))*4)+d'), '<a>(((<b><c></c></b>))*4)<d></d></a>');
equal(str('(div>dl>(dt+dd)*2)'), '(<div><dl>(<dt></dt><dd></dd>)*2</dl></div>)');
equal(str('a>()'), '<a>()</a>');
});
it('attributes', () => {
equal(str('[].foo'), '<? class=foo></?>');
equal(str('[a]'), '<? a></?>');
equal(str('[a b c [d]]'), '<? a b c [d]></?>');
equal(str('[a=b]'), '<? a=b></?>');
equal(str('[a=b c= d=e]'), '<? a=b c d=e></?>');
equal(str('[a=b.c d=тест]'), '<? a=b.c d=тест></?>');
equal(str('[[a]=b (c)=d]'), '<? [a]=b (c)=d></?>');
// Quoted attribute values
equal(str('[a="b"]'), '<? a="b"></?>');
equal(str('[a="b" c=\'d\' e=""]'), '<? a="b" c=\'d\' e=""></?>');
equal(str('[[a]="b" (c)=\'d\']'), '<? [a]="b" (c)=\'d\'></?>');
// Mixed quoted
equal(str('[a="foo\'bar" b=\'foo"bar\' c="foo\\\"bar"]'), '<? a="foo\'bar" b=\'foo"bar\' c="foo"bar"></?>');
// Boolean & implied attributes
equal(str('[a. b.]'), '<? a. b.></?>');
equal(str('[!a !b.]'), '<? !a !b.></?>');
// Default values
equal(str('["a.b"]'), '<? ?="a.b"></?>');
equal(str('[\'a.b\' "c=d" foo=bar "./test.html"]'), '<? ?=\'a.b\' ?="c=d" foo=bar ?="./test.html"></?>');
// Expressions as values
equal(str('[foo={1 + 2} bar={fn(1, "foo")}]'), '<? foo={1 + 2} bar={fn(1, "foo")}></?>');
// Tabstops as unquoted values
equal(str('[name=${1} value=${2:test}]'), '<? name=${1} value=${2:test}></?>');
});
it('malformed attributes', () => {
equal(str('[a'), '<? a></?>');
equal(str('[a={foo]'), '<? a={foo]></?>');
throws(() => str('[a="foo]'), /Unclosed quote/);
throws(() => str('[a=b=c]'), /Unexpected "Operator" token/);
});
it('elements', () => {
equal(str('div'), '<div></div>');
equal(str('div.foo'), '<div class=foo></div>');
equal(str('div#foo'), '<div id=foo></div>');
equal(str('div#foo.bar'), '<div id=foo class=bar></div>');
equal(str('div.foo#bar'), '<div class=foo id=bar></div>');
equal(str('div.foo.bar.baz'), '<div class=foo class=bar class=baz></div>');
equal(str('.foo'), '<? class=foo></?>');
equal(str('#foo'), '<? id=foo></?>');
equal(str('.foo_bar'), '<? class=foo_bar></?>');
equal(str('#foo.bar'), '<? id=foo class=bar></?>');
// Attribute shorthands
equal(str('.'), '<? class></?>');
equal(str('#'), '<? id></?>');
equal(str('#.'), '<? id class></?>');
equal(str('.#.'), '<? class id class></?>');
equal(str('.a..'), '<? class=a class></?>');
// Elements with attributes
equal(str('div[foo=bar]'), '<div foo=bar></div>');
equal(str('div.a[b=c]'), '<div class=a b=c></div>');
equal(str('div.mr-\\[500\\][a=b]'), '<div class=mr-[500] a=b></div>');
equal(str('div[b=c].a'), '<div b=c class=a></div>');
equal(str('div[a=b][c="d"]'), '<div a=b c="d"></div>');
equal(str('[b=c]'), '<? b=c></?>');
equal(str('.a\\[b-c\\]'), '<? class=a[b-c]></?>');
equal(str('."a:[b-c]"'), '<? class=a:[b-c]></?>');
equal(str('."peer-[.is-dirty]:peer-required:block"'), '<? class=peer-[.is-dirty]:peer-required:block></?>');
equal(str('."mr-50"."peer-[:nth-of-type(3)_&]:block"'), '<? class=mr-50 class=peer-[:nth-of-type(3)_&]:block></?>');
equal(str('.a[b=c]'), '<? class=a b=c></?>');
equal(str('[b=c].a#d'), '<? b=c class=a id=d></?>');
equal(str('[b=c]a'), '<? b=c></?><a></a>', 'Do not consume node name after attribute set');
// Element with text
equal(str('div{foo}'), '<div>foo</div>');
equal(str('{foo}'), '<?>foo</?>');
// Mixed
equal(str('div.foo{bar}'), '<div class=foo>bar</div>');
equal(str('.foo{bar}#baz'), '<? class=foo id=baz>bar</?>');
equal(str('.foo[b=c]{bar}'), '<? class=foo b=c>bar</?>');
// Repeated element
equal(str('div.foo*3'), '<div*3 class=foo></div>');
equal(str('.foo*'), '<?* class=foo></?>');
equal(str('.a[b=c]*10'), '<?*10 class=a b=c></?>');
equal(str('.a*10[b=c]'), '<?*10 class=a b=c></?>');
equal(str('.a*10{text}'), '<?*10 class=a>text</?>');
// Self-closing element
equal(str('div/'), '<div />');
equal(str('.foo/'), '<? class=foo />');
equal(str('.foo[bar]/'), '<? class=foo bar />');
equal(str('.foo/*3'), '<?*3 class=foo />');
equal(str('.foo*3/'), '<?*3 class=foo />');
throws(() => parse('/'), /Unexpected character/);
});
it('JSX', () => {
const opt = { jsx: true };
equal(str('foo.bar', opt), '<foo class=bar></foo>');
equal(str('Foo.bar', opt), '<Foo class=bar></Foo>');
equal(str('Foo.Bar', opt), '<Foo.Bar></Foo.Bar>');
equal(str('Foo.', opt), '<Foo class></Foo>');
equal(str('Foo.Bar.baz', opt), '<Foo.Bar class=baz></Foo.Bar>');
equal(str('Foo.Bar.Baz', opt), '<Foo.Bar.Baz></Foo.Bar.Baz>');
equal(str('.{theme.class}', opt), '<? class=theme.class></?>');
equal(str('#{id}', opt), '<? id=id></?>');
equal(str('Foo.{theme.class}', opt), '<Foo class=theme.class></Foo>');
});
it('errors', () => {
throws(() => parse('str?'), /Unexpected character at 4/);
throws(() => parse('foo,bar'), /Unexpected character at 4/);
equal(str('foo\\,bar'), '<foo,bar></foo,bar>');
equal(str('foo\\'), '<foo></foo>');
});
it('missing braces', () => {
// Do not throw errors on missing closing braces
equal(str('div[title="test"'), '<div title="test"></div>');
equal(str('div(foo'), '<div></div>(<foo></foo>)');
equal(str('div{foo'), '<div>foo</div>');
});
});
================================================
FILE: packages/abbreviation/test/tokenizer.ts
================================================
import { describe, it } from 'node:test';
import { deepStrictEqual } from 'node:assert';
import tokenize from '../src/tokenizer';
describe('Tokenizer', () => {
it('basic abbreviations', () => {
deepStrictEqual(tokenize('ul>li'), [
{ type: 'Literal', value: 'ul', start: 0, end: 2 },
{ type: 'Operator', operator: 'child', start: 2, end: 3 },
{ type: 'Literal', value: 'li', start: 3, end: 5 }
]);
deepStrictEqual(tokenize('ul[title="foo+bar\'str\'" (attr)=bar]{(some > text)}'), [
{ type: 'Literal', value: 'ul', start: 0, end: 2 },
{ type: 'Bracket', open: true, context: 'attribute', start: 2, end: 3 },
{ type: 'Literal', value: 'title', start: 3, end: 8 },
{ type: 'Operator', operator: 'equal', start: 8, end: 9 },
{ type: 'Quote', single: false, start: 9, end: 10 },
{ type: 'Literal', value: 'foo+bar\'str\'', start: 10, end: 22 },
{ type: 'Quote', single: false, start: 22, end: 23 },
{ type: 'WhiteSpace', start: 23, end: 24, value: ' ' },
{ type: 'Bracket', open: true, context: 'group', start: 24, end: 25 },
{ type: 'Literal', value: 'attr', start: 25, end: 29 },
{ type: 'Bracket', open: false, context: 'group', start: 29, end: 30 },
{ type: 'Operator', operator: 'equal', start: 30, end: 31 },
{ type: 'Literal', value: 'bar', start: 31, end: 34 },
{ type: 'Bracket', open: false, context: 'attribute', start: 34, end: 35 },
{ type: 'Bracket', open: true, context: 'expression', start: 35, end: 36 },
{ type: 'Literal', value: '(some > text)', start: 36, end: 49 },
{ type: 'Bracket', open: false, context: 'expression', start: 49, end: 50 }
]);
deepStrictEqual(tokenize('h${some${1:field placeholder}}'), [
{ type: 'Literal', value: 'h', start: 0, end: 1 },
{ type: 'RepeaterNumber', size: 1, parent: 0, reverse: false, base: 1, start: 1, end: 2 },
{ type: 'Bracket', open: true, context: 'expression', start: 2, end: 3 },
{ type: 'Literal', value: 'some', start: 3, end: 7 },
{ type: 'Field', index: 1, name: 'field placeholder', start: 7, end: 29 },
{ type: 'Bracket', open: false, context: 'expression', start: 29, end: 30 }
]);
deepStrictEqual(tokenize('div{[}+a{}'), [
{ type: 'Literal', value: 'div', start: 0, end: 3 },
{ type: 'Bracket', open: true, context: 'expression', start: 3, end: 4 },
{ type: 'Literal', value: '[', start: 4, end: 5 },
{ type: 'Bracket', open: false, context: 'expression', start: 5, end: 6 },
{ type: 'Operator', operator: 'sibling', start: 6, end: 7 },
{ type: 'Literal', value: 'a', start: 7, end: 8 },
{ type: 'Bracket', open: true, context: 'expression', start: 8, end: 9 },
{ type: 'Bracket', open: false, context: 'expression', start: 9, end: 10 }
]);
});
it('repeater', () => {
deepStrictEqual(tokenize('#sample*3'), [
{ type: 'Operator', operator: 'id', start: 0, end: 1 },
{ type: 'Literal', value: 'sample', start: 1, end: 7 },
{ type: 'Repeater', count: 3, value: 0, implicit: false, start: 7, end: 9 }
]);
deepStrictEqual(tokenize('div[foo*3]'), [
{ type: 'Literal', value: 'div', start: 0, end: 3 },
{ type: 'Bracket', open: true, context: 'attribute', start: 3, end: 4 },
{ type: 'Literal', value: 'foo*3', start: 4, end: 9 },
{ type: 'Bracket', open: false, context: 'attribute', start: 9, end: 10 }
]);
deepStrictEqual(tokenize('({a*2})*3'), [
{ type: 'Bracket', open: true, context: 'group', start: 0, end: 1 },
{ type: 'Bracket', open: true, context: 'expression', start: 1, end: 2 },
{ type: 'Literal', value: 'a*2', start: 2, end: 5 },
{ type: 'Bracket', open: false, context: 'expression', start: 5, end: 6 },
{ type: 'Bracket', open: false, context: 'group', start: 6, end: 7 },
{ type: 'Repeater', count: 3, value: 0, implicit: false, start: 7, end: 9 }
]);
});
});
================================================
FILE: packages/abbreviation/tsconfig.json
================================================
{
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "./dist"
},
"include": ["src/**/*.ts"]
}
================================================
FILE: packages/css-abbreviation/.npmignore
================================================
npm-debug.log*
node_modules
jspm_packages
.npm
/.*
/*.*
/test
/src
================================================
FILE: packages/css-abbreviation/LICENSE
================================================
MIT License
Copyright (c) 2020 Sergey Chikuyonok <serge.che@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: packages/css-abbreviation/README.md
================================================
# Emmet stylesheet abbreviation parser
Parses 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.
Unlike in [markup abbreviations](/packages/abbreviation), elements in stylesheet abbreviations cannot be nested and contain attributes, but allow embedded values in element names.
## Usage
You can install it via npm:
```bash
npm install @emmetio/css-abbreviation
```
Then add it into your project:
```js
import parse from '@emmetio/css-abbreviation';
const props = parse('p10+poa');
/* [{
name: 'p',
value: [{ type: 'CSSValue', value: [...] }],
important: false
}, {
name: 'poa',
value: [],
important: false
}] */
```
The returned result is an array of `CSSProperty` items: a node with name and values.
## Abbreviation syntax
Emmet 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.
### Operators
Since CSS properties can’t be nested, the only available operator is `+`.
================================================
FILE: packages/css-abbreviation/package.json
================================================
{
"name": "@emmetio/css-abbreviation",
"version": "2.1.8",
"description": "Parses Emmet CSS abbreviation into AST tree",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"type": "module",
"exports": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"scripts": {
"test": "tsx --test ./test/*.ts",
"build": "rollup -c",
"watch": "rollup -wc",
"clean": "rm -rf ./dist",
"prepublishOnly": "npm test &&npm run clean && npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/emmetio/emmet.git"
},
"keywords": [],
"author": "Sergey Chikuyonok <serge.che@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/emmetio/emmet/issues"
},
"homepage": "https://github.com/emmetio/emmet#readme",
"dependencies": {
"@emmetio/scanner": "^1.0.4"
},
"directories": {
"test": "test"
}
}
================================================
FILE: packages/css-abbreviation/rollup.config.js
================================================
import typescript from '@rollup/plugin-typescript';
/** @type {import('rollup').RollupOptions} */
export default {
input: './src/index.ts',
external: ['@emmetio/scanner'],
plugins: [typescript()],
output: [{
format: 'cjs',
file: 'dist/index.cjs',
sourcemap: true,
exports: 'named',
}, {
format: 'es',
file: 'dist/index.js',
sourcemap: true,
}]
};
================================================
FILE: packages/css-abbreviation/src/index.ts
================================================
import { ScannerError } from '@emmetio/scanner';
import tokenize, { getToken, type AllTokens } from './tokenizer';
import parser, { type CSSProperty, type ParseOptions } from './parser';
export { tokenize, getToken, parser };
export * from './tokenizer';
export type { CSSProperty, CSSValue, ParseOptions, FunctionCall, Value } from './parser';
export type CSSAbbreviation = CSSProperty[];
/**
* Parses given abbreviation into property set
*/
export default function parse(abbr: string | AllTokens[], options?: ParseOptions): CSSAbbreviation {
try {
const tokens = typeof abbr === 'string' ? tokenize(abbr, options && options.value) : abbr;
return parser(tokens, options);
} catch (err) {
if (err instanceof ScannerError && typeof abbr === 'string') {
err.message += `\n${abbr}\n${'-'.repeat(err.pos)}^`;
}
throw err;
}
}
================================================
FILE: packages/css-abbreviation/src/parser/TokenScanner.ts
================================================
import type { AllTokens } from '../tokenizer/index.js';
export interface TokenScanner {
tokens: AllTokens[];
start: number;
pos: number;
size: number;
}
type TestFn = (token?: AllTokens) => boolean;
export default function tokenScanner(tokens: AllTokens[]): TokenScanner {
return {
tokens,
start: 0,
pos: 0,
size: tokens.length
};
}
export function peek(scanner: TokenScanner): AllTokens | undefined {
return scanner.tokens[scanner.pos];
}
export function next(scanner: TokenScanner): AllTokens | undefined {
return scanner.tokens[scanner.pos++];
}
export function slice(scanner: TokenScanner, from = scanner.start, to = scanner.pos): AllTokens[] {
return scanner.tokens.slice(from, to);
}
export function readable(scanner: TokenScanner): boolean {
return scanner.pos < scanner.size;
}
export function consume(scanner: TokenScanner, test: TestFn): boolean {
if (test(peek(scanner))) {
scanner.pos++;
return true;
}
return false;
}
export function error(scanner: TokenScanner, message: string, token = peek(scanner)) {
if (token && token.start != null) {
message += ` at ${token.start}`;
}
const err = new Error(message);
err['pos'] = token && token.start;
return err;
}
export function consumeWhile(scanner: TokenScanner, test: TestFn): boolean {
const start = scanner.pos;
while (consume(scanner, test)) { /* */ }
return scanner.pos !== start;
}
================================================
FILE: packages/css-abbreviation/src/parser/index.ts
================================================
import { OperatorType } from '../tokenizer/tokens.js';
import type { StringValue, NumberValue, ColorValue, Literal, AllTokens, Bracket, WhiteSpace, Operator, Field, CustomProperty } from '../tokenizer/tokens.js';
import tokenScanner, { type TokenScanner, readable, peek, consume, error } from './TokenScanner.js';
export type Value = StringValue | NumberValue | ColorValue | Literal | FunctionCall | Field | CustomProperty;
export interface FunctionCall {
type: 'FunctionCall';
name: string;
arguments: CSSValue[];
}
export interface CSSValue {
type: 'CSSValue';
value: Value[];
}
export interface CSSProperty {
name?: string;
value: CSSValue[];
important: boolean;
/** Snippet matched with current property */
snippet?: any;
}
export interface ParseOptions {
/** Consumes given abbreviation tokens as value */
value?: boolean;
}
export default function parser(tokens: AllTokens[], options: ParseOptions = {}): CSSProperty[] {
const scanner = tokenScanner(tokens);
const result: CSSProperty[] = [];
let property: CSSProperty | undefined;
while (readable(scanner)) {
if (property = consumeProperty(scanner, options)) {
result.push(property);
} else if (!consume(scanner, isSiblingOperator)) {
throw error(scanner, 'Unexpected token');
}
}
return result;
}
/**
* Consumes single CSS property
*/
function consumeProperty(scanner: TokenScanner, options: ParseOptions): CSSProperty | undefined {
let name: string | undefined;
let important = false;
let valueFragment: CSSValue | undefined;
const value: CSSValue[] = [];
const token = peek(scanner)!;
const valueMode = !!options.value;
if (!valueMode && isLiteral(token) && !isFunctionStart(scanner)) {
scanner.pos++;
name = token.value;
// Consume any following value delimiter after property name
consume(scanner, isValueDelimiter);
}
// Skip whitespace right after property name, if any
if (valueMode) {
consume(scanner, isWhiteSpace);
}
while (readable(scanner)) {
if (consume(scanner, isImportant)) {
important = true;
} else if (valueFragment = consumeValue(scanner, valueMode)) {
value.push(valueFragment);
} else if (!consume(scanner, isFragmentDelimiter)) {
break;
}
}
if (name || value.length || important) {
return { name, value, important };
}
}
/**
* Consumes single value fragment, e.g. all value tokens before comma
*/
function consumeValue(scanner: TokenScanner, inArgument: boolean): CSSValue | undefined {
const result: Value[] = [];
let token: AllTokens | undefined;
let args: CSSValue[] | undefined;
while (readable(scanner)) {
token = peek(scanner)!;
if (isValue(token)) {
scanner.pos++;
if (isLiteral(token) && (args = consumeArguments(scanner))) {
result.push({
type: 'FunctionCall',
name: token.value,
arguments: args
} as FunctionCall);
} else {
result.push(token);
}
} else if (isValueDelimiter(token) || (inArgument && isWhiteSpace(token))) {
scanner.pos++;
} else {
break;
}
}
return result.length
? { type: 'CSSValue', value: result }
: void 0;
}
function consumeArguments(scanner: TokenScanner): CSSValue[] | undefined {
const start = scanner.pos;
if (consume(scanner, isOpenBracket)) {
const args: CSSValue[] = [];
let value: CSSValue | undefined;
while (readable(scanner) && !consume(scanner, isCloseBracket)) {
if (value = consumeValue(scanner, true)) {
args.push(value);
} else if (!consume(scanner, isWhiteSpace) && !consume(scanner, isArgumentDelimiter)) {
throw error(scanner, 'Unexpected token');
}
}
scanner.start = start;
return args;
}
}
function isLiteral(token: AllTokens): token is Literal {
return token && token.type === 'Literal';
}
function isBracket(token: AllTokens, open?: boolean): token is Bracket {
return token && token.type === 'Bracket' && (open == null || token.open === open);
}
function isOpenBracket(token: AllTokens) {
return isBracket(token, true);
}
function isCloseBracket(token: AllTokens) {
return isBracket(token, false);
}
function isWhiteSpace(token: AllTokens): token is WhiteSpace {
return token && token.type === 'WhiteSpace';
}
function isOperator(token: AllTokens, operator?: OperatorType): token is Operator {
return token && token.type === 'Operator' && (!operator || token.operator === operator);
}
function isSiblingOperator(token: AllTokens) {
return isOperator(token, OperatorType.Sibling);
}
function isArgumentDelimiter(token: AllTokens) {
return isOperator(token, OperatorType.ArgumentDelimiter);
}
function isFragmentDelimiter(token: AllTokens) {
return isArgumentDelimiter(token);
}
function isImportant(token: AllTokens) {
return isOperator(token, OperatorType.Important);
}
function isValue(token: AllTokens): token is StringValue | NumberValue | ColorValue | Literal {
return token.type === 'StringValue'
|| token.type === 'ColorValue'
|| token.type === 'NumberValue'
|| token.type === 'Literal'
|| token.type === 'Field'
|| token.type === 'CustomProperty';
}
function isValueDelimiter(token: AllTokens): boolean {
return isOperator(token, OperatorType.PropertyDelimiter)
|| isOperator(token, OperatorType.ValueDelimiter);
}
function isFunctionStart(scanner: TokenScanner): boolean {
const t1 = scanner.tokens[scanner.pos];
const t2 = scanner.tokens[scanner.pos + 1];
return t1 && t2 && isLiteral(t1) && t2.type === 'Bracket';
}
================================================
FILE: packages/css-abbreviation/src/tokenizer/index.ts
================================================
import { default as Scanner, isAlphaWord, isAlpha, isNumber, isAlphaNumericWord, isSpace, isQuote } from '@emmetio/scanner';
import { OperatorType } from './tokens';
import type { AllTokens, Literal, NumberValue, ColorValue, WhiteSpace, Operator, Bracket, StringValue, Field, CustomProperty } from './tokens';
import { Chars } from './utils';
export * from './tokens';
export default function tokenize(abbr: string, isValue?: boolean): AllTokens[] {
let brackets = 0;
let token: AllTokens | undefined;
const scanner = new Scanner(abbr);
const tokens: AllTokens[] = [];
while (!scanner.eof()) {
token = getToken(scanner, brackets === 0 && !isValue);
if (!token) {
throw scanner.error('Unexpected character');
}
if (token.type === 'Bracket') {
if (!brackets && token.open) {
mergeTokens(scanner, tokens);
}
brackets += token.open ? 1 : -1;
if (brackets < 0) {
throw scanner.error('Unexpected bracket', token.start);
}
}
tokens.push(token);
// Forcibly consume next operator after unit-less numeric value or color:
// next dash `-` must be used as value delimiter
if (shouldConsumeDashAfter(token) && (token = operator(scanner))) {
tokens.push(token);
}
}
return tokens;
}
/**
* Returns next token from given scanner, if possible
*/
export function getToken(scanner: Scanner, short?: boolean) {
return field(scanner)
|| customProperty(scanner)
|| numberValue(scanner)
|| colorValue(scanner)
|| stringValue(scanner)
|| bracket(scanner)
|| operator(scanner)
|| whiteSpace(scanner)
|| literal(scanner, short);
}
function field(scanner: Scanner): Field | undefined {
const start = scanner.pos;
if (scanner.eat(Chars.Dollar) && scanner.eat(Chars.CurlyBracketOpen)) {
scanner.start = scanner.pos;
let index: number | undefined;
let name: string = '';
if (scanner.eatWhile(isNumber)) {
// It’s a field
index = Number(scanner.current());
name = scanner.eat(Chars.Colon) ? consumePlaceholder(scanner) : '';
} else if (isAlpha(scanner.peek())) {
// It’s a variable
name = consumePlaceholder(scanner);
}
if (scanner.eat(Chars.CurlyBracketClose)) {
return {
type: 'Field',
index, name,
start,
end: scanner.pos
};
}
throw scanner.error('Expecting }');
}
// If we reached here then there’s no valid field here, revert
// back to starting position
scanner.pos = start;
}
/**
* Consumes a placeholder: value right after `:` in field. Could be empty
*/
function consumePlaceholder(stream: Scanner): string {
const stack: number[] = [];
stream.start = stream.pos;
while (!stream.eof()) {
if (stream.eat(Chars.CurlyBracketOpen)) {
stack.push(stream.pos);
} else if (stream.eat(Chars.CurlyBracketClose)) {
if (!stack.length) {
stream.pos--;
break;
}
stack.pop();
} else {
stream.pos++;
}
}
if (stack.length) {
stream.pos = stack.pop()!;
throw stream.error(`Expecting }`);
}
return stream.current();
}
/**
* Consumes literal from given scanner
* @param short Use short notation for consuming value.
* The difference between “short” and “full” notation is that first one uses
* alpha characters only and used for extracting keywords from abbreviation,
* while “full” notation also supports numbers and dashes
*/
function literal(scanner: Scanner, short?: boolean): Literal | undefined {
const start = scanner.pos;
if (scanner.eat(isIdentPrefix)) {
// SCSS or LESS variable
// NB a bit dirty hack: if abbreviation starts with identifier prefix,
// consume alpha characters only to allow embedded variables
scanner.eatWhile(start ? isKeyword : isLiteral);
} else if (scanner.eat(isAlphaWord)) {
scanner.eatWhile(short ? isLiteral : isKeyword);
} else {
// Allow dots only at the beginning of literal
scanner.eat(Chars.Dot);
scanner.eatWhile(isLiteral);
}
if (start !== scanner.pos) {
scanner.start = start;
return createLiteral(scanner, scanner.start = start);
}
}
function createLiteral(scanner: Scanner, start = scanner.start, end = scanner.pos): Literal {
return {
type: 'Literal',
value: scanner.substring(start, end),
start,
end
};
}
/**
* Consumes numeric CSS value (number with optional unit) from current stream,
* if possible
*/
function numberValue(scanner: Scanner): NumberValue | undefined {
const start = scanner.pos;
if (consumeNumber(scanner)) {
scanner.start = start;
const rawValue = scanner.current();
// eat unit, which can be a % or alpha word
scanner.start = scanner.pos;
scanner.eat(Chars.Percent) || scanner.eatWhile(isAlphaWord);
return {
type: 'NumberValue',
value: Number(rawValue),
rawValue,
unit: scanner.current(),
start,
end: scanner.pos
};
}
}
/**
* Consumes quoted string value from given scanner
*/
function stringValue(scanner: Scanner): StringValue | undefined {
const ch = scanner.peek();
const start = scanner.pos;
let finished = false;
if (isQuote(ch)) {
scanner.pos++;
while (!scanner.eof()) {
// Do not throw error on malformed string
if (scanner.eat(ch)) {
finished = true;
break;
} else {
scanner.pos++;
}
}
scanner.start = start;
return {
type: 'StringValue',
value: scanner.substring(start + 1, scanner.pos - (finished ? 1 : 0)),
quote: ch === Chars.SingleQuote ? 'single' : 'double',
start,
end: scanner.pos
};
}
}
/**
* Consumes a color token from given string
*/
function colorValue(scanner: Scanner): ColorValue | Literal | undefined {
// supported color variations:
// #abc → #aabbccc
// #0 → #000000
// #fff.5 → rgba(255, 255, 255, 0.5)
// #t → transparent
const start = scanner.pos;
if (scanner.eat(Chars.Hash)) {
const valueStart = scanner.pos;
let color = '';
let alpha = '';
if (scanner.eatWhile(isHex)) {
color = scanner.substring(valueStart, scanner.pos);
alpha = colorAlpha(scanner);
} else if (scanner.eat(Chars.Transparent)) {
color = '0';
alpha = colorAlpha(scanner) || '0';
} else {
alpha = colorAlpha(scanner);
}
if (color || alpha || scanner.eof()) {
const { r, g, b, a } = parseColor(color, alpha);
return {
type: 'ColorValue',
r, g, b, a,
raw: scanner.substring(start + 1, scanner.pos),
start,
end: scanner.pos
};
} else {
// Consumed # but no actual value: invalid color value, treat it as literal
return createLiteral(scanner, start);
}
}
scanner.pos = start;
}
/**
* Consumes alpha value of color: `.1`
*/
function colorAlpha(scanner: Scanner): string {
const start = scanner.pos;
if (scanner.eat(Chars.Dot)) {
scanner.start = start;
if (scanner.eatWhile(isNumber)) {
return scanner.current();
}
return '1';
}
return '';
}
/**
* Consumes white space characters as string literal from given scanner
*/
function whiteSpace(scanner: Scanner): WhiteSpace | undefined {
const start = scanner.pos;
if (scanner.eatWhile(isSpace)) {
return {
type: 'WhiteSpace',
start,
end: scanner.pos
};
}
}
/**
* Consumes custom CSS property: --foo-bar
*/
function customProperty(scanner: Scanner): CustomProperty | undefined {
const start = scanner.pos;
if (scanner.eat(Chars.Dash) && scanner.eat(Chars.Dash)) {
scanner.start = start;
scanner.eatWhile(isKeyword);
return {
type: 'CustomProperty',
value: scanner.current(),
start,
end: scanner.pos
};
}
scanner.pos = start;
}
/**
* Consumes bracket from given scanner
*/
function bracket(scanner: Scanner): Bracket | undefined {
const ch = scanner.peek();
if (isBracket(ch)) {
return {
type: 'Bracket',
open: ch === Chars.RoundBracketOpen,
start: scanner.pos++,
end: scanner.pos
};
}
}
/**
* Consumes operator from given scanner
*/
function operator(scanner: Scanner): Operator | undefined {
const op = operatorType(scanner.peek());
if (op) {
return {
type: 'Operator',
operator: op,
start: scanner.pos++,
end: scanner.pos
};
}
}
/**
* Eats number value from given stream
* @return Returns `true` if number was consumed
*/
function consumeNumber(stream: Scanner): boolean {
const start = stream.pos;
stream.eat(Chars.Dash);
const afterNegative = stream.pos;
const hasDecimal = stream.eatWhile(isNumber);
const prevPos = stream.pos;
if (stream.eat(Chars.Dot)) {
// It’s perfectly valid to have numbers like `1.`, which enforces
// value to float unit type
const hasFloat = stream.eatWhile(isNumber);
if (!hasDecimal && !hasFloat) {
// Lone dot
stream.pos = prevPos;
}
}
// Edge case: consumed dash only: not a number, bail-out
if (stream.pos === afterNegative) {
stream.pos = start;
}
return stream.pos !== start;
}
function isIdentPrefix(code: number): boolean {
return code === Chars.At || code === Chars.Dollar;
}
/**
* If given character is an operator, returns it’s type
*/
function operatorType(ch: number): OperatorType | undefined {
return (ch === Chars.Sibling && OperatorType.Sibling)
|| (ch === Chars.Excl && OperatorType.Important)
|| (ch === Chars.Comma && OperatorType.ArgumentDelimiter)
|| (ch === Chars.Colon && OperatorType.PropertyDelimiter)
|| (ch === Chars.Dash && OperatorType.ValueDelimiter)
|| void 0;
}
/**
* Check if given code is a hex value (/0-9a-f/)
*/
function isHex(code: number): boolean {
return isNumber(code) || isAlpha(code, 65, 70); // A-F
}
function isKeyword(code: number): boolean {
return isAlphaNumericWord(code) || code === Chars.Dash;
}
function isBracket(code: number) {
return code === Chars.RoundBracketOpen || code === Chars.RoundBracketClose;
}
function isLiteral(code: number) {
return isAlphaWord(code) || code === Chars.Percent || code === Chars.Slash;
}
/**
* Parses given color value from abbreviation into RGBA format
*/
function parseColor(value: string, alpha?: string): { r: number, g: number, b: number, a: number } {
let r = '0';
let g = '0';
let b = '0';
let a = Number(alpha != null && alpha !== '' ? alpha : 1);
if (value === 't') {
a = 0;
} else {
switch (value.length) {
case 0:
break;
case 1:
r = g = b = value + value;
break;
case 2:
r = g = b = value;
break;
case 3:
r = value[0] + value[0];
g = value[1] + value[1];
b = value[2] + value[2];
break;
default:
value += value;
r = value.slice(0, 2);
g = value.slice(2, 4);
b = value.slice(4, 6);
}
}
return {
r: parseInt(r, 16),
g: parseInt(g, 16),
b: parseInt(b, 16),
a
};
}
/**
* Check if scanner reader must consume dash after given token.
* Used in cases where user must explicitly separate numeric values
*/
function shouldConsumeDashAfter(token: AllTokens): boolean {
return token.type === 'ColorValue' || (token.type === 'NumberValue' && !token.unit);
}
/**
* Merges last adjacent tokens into a single literal.
* This function is used to overcome edge case when function name was parsed
* as a list of separate tokens. For example, a `scale3d()` value will be
* parsed as literal and number tokens (`scale` and `3d`) which is a perfectly
* valid abbreviation but undesired result. This function will detect last adjacent
* literal and number values and combine them into single literal
*/
function mergeTokens(scanner: Scanner, tokens: AllTokens[]) {
let start = 0;
let end = 0;
while (tokens.length) {
const token = last(tokens)!;
if (token.type === 'Literal' || token.type === 'NumberValue') {
start = token.start!;
if (!end) {
end = token.end!;
}
tokens.pop();
} else {
break;
}
}
if (start !== end) {
tokens.push(createLiteral(scanner, start, end));
}
}
function last<T>(arr: T[]): T | undefined {
return arr[arr.length - 1];
}
================================================
FILE: packages/css-abbreviation/src/tokenizer/tokens.ts
================================================
export type AllTokens = Bracket | Literal | Operator | WhiteSpace | ColorValue
| NumberValue | StringValue | CustomProperty | Field;
export const enum OperatorType {
Sibling = '+',
Important = '!',
ArgumentDelimiter = ',',
ValueDelimiter = '-',
PropertyDelimiter = ':'
}
export interface Token {
type: string;
/** Location of token start in source */
start?: number;
/** Location of token end in source */
end?: number;
}
export interface Operator extends Token {
type: 'Operator';
operator: OperatorType;
}
export interface Bracket extends Token {
type: 'Bracket';
open: boolean;
}
export interface Literal extends Token {
type: 'Literal';
value: string;
}
export interface CustomProperty extends Token {
type: 'CustomProperty';
value: string;
}
export interface NumberValue extends Token {
type: 'NumberValue';
value: number;
unit: string;
rawValue: string;
}
export interface ColorValue extends Token {
type: 'ColorValue';
r: number;
g: number;
b: number;
a: number;
raw: string;
}
export interface StringValue extends Token {
type: 'StringValue';
value: string;
quote: 'single' | 'double';
}
export interface WhiteSpace extends Token {
type: 'WhiteSpace';
}
export interface Field extends Token {
type: 'Field';
index?: number;
name: string;
}
================================================
FILE: packages/css-abbreviation/src/tokenizer/utils.ts
================================================
export const enum Chars {
/** `#` character */
Hash = 35,
/** `$` character */
Dollar = 36,
/** `-` character */
Dash = 45,
/** `.` character */
Dot = 46,
/** `:` character */
Colon = 58,
/** `,` character */
Comma = 44,
/** `!` character */
Excl = 33,
/** `@` character */
At = 64,
/** `%` character */
Percent = 37,
/** `_` character */
Underscore = 95,
/** `(` character */
RoundBracketOpen = 40,
/** `)` character */
RoundBracketClose = 41,
/** `{` character */
CurlyBracketOpen = 123,
/** `}` character */
CurlyBracketClose = 125,
/** `+` character */
Sibling = 43,
/** `'` character */
SingleQuote = 39,
/** `"` character */
DoubleQuote = 34,
/** `t` character */
Transparent = 116,
/** `/` character */
Slash = 47,
}
================================================
FILE: packages/css-abbreviation/test/assets/stringify.ts
================================================
import type { CSSProperty, CSSValue, Value } from '../../src/parser';
export default function stringify(prop: CSSProperty): string {
return `${prop.name || '?'}: ${prop.value.map(stringifyValue).join(', ')}${prop.important ? ' !important' : ''};`;
}
function stringifyValue(value: CSSValue): string {
return value.value.map(stringifyToken).join(' ');
}
function stringifyToken(token: Value): string {
if (token.type === 'ColorValue') {
const { r, g, b, a } = token;
if (!r && !g && !b && !a) {
return 'transparent';
}
if (a === 1) {
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
return `rgba(${r}, ${g}, ${b}, ${a})`;
} else if (token.type === 'NumberValue') {
return `${token.value}${token.unit}`;
} else if (token.type === 'StringValue') {
return `"${token.value}"`;
} else if (token.type === 'Literal') {
return token.value;
} else if (token.type === 'FunctionCall') {
return `${token.name}(${token.arguments.map(stringifyValue).join(', ')})`;
}
throw new Error('Unexpected token');
}
function toHex(num: number): string {
return pad(num.toString(16), 2);
}
function pad(value: string, len: number): string {
while (value.length < len) {
value = '0' + value;
}
return value;
}
================================================
FILE: packages/css-abbreviation/test/parser.ts
================================================
import { describe, it } from 'node:test';
import { strictEqual as equal, throws } from 'node:assert';
import parser, { ParseOptions, FunctionCall } from '../src/parser';
import tokenizer from '../src/tokenizer';
import stringify from './assets/stringify';
const parse = (abbr: string, opt?: ParseOptions) => parser(tokenizer(abbr), opt);
const expand = (abbr: string) => parse(abbr).map(stringify).join('');
describe('CSS Abbreviation parser', () => {
it('basic', () => {
equal(expand('p10!'), 'p: 10 !important;');
equal(expand('p-10-20'), 'p: -10 20;');
equal(expand('p-10%-20--30'), 'p: -10% -20 -30;');
equal(expand('p.5'), 'p: 0.5;');
equal(expand('p-.5'), 'p: -0.5;');
equal(expand('p.1.2.3'), 'p: 0.1 0.2 0.3;');
equal(expand('p.1-.2.3'), 'p: 0.1 0.2 0.3;');
equal(expand('10'), '?: 10;');
equal(expand('.1'), '?: 0.1;');
equal(expand('lh1.'), 'lh: 1;');
});
it('color', () => {
equal(expand('c#'), 'c: #000000;');
equal(expand('c#1'), 'c: #111111;');
equal(expand('c#f'), 'c: #ffffff;');
equal(expand('c#a#b#c'), 'c: #aaaaaa #bbbbbb #cccccc;');
equal(expand('c#af'), 'c: #afafaf;');
equal(expand('c#fc0'), 'c: #ffcc00;');
equal(expand('c#11.5'), 'c: rgba(17, 17, 17, 0.5);');
equal(expand('c#.99'), 'c: rgba(0, 0, 0, 0.99);');
equal(expand('c#t'), 'c: transparent;');
});
it('keywords', () => {
equal(expand('m:a'), 'm: a;');
equal(expand('m-a'), 'm: a;');
equal(expand('m-abc'), 'm: abc;');
equal(expand('m-a0'), 'm: a 0;');
equal(expand('m-a0-a'), 'm: a 0 a;');
});
it('functions', () => {
equal(expand('bg-lg(top, "red, black",rgb(0, 0, 0) 10%)'), 'bg: lg(top, "red, black", rgb(0, 0, 0) 10%);');
equal(expand('lg(top, "red, black",rgb(0, 0, 0) 10%)'), '?: lg(top, "red, black", rgb(0, 0, 0) 10%);');
});
it('mixed', () => {
equal(expand('bd1-s#fc0'), 'bd: 1 s #ffcc00;');
equal(expand('bd#fc0-1'), 'bd: #ffcc00 1;');
equal(expand('p0+m0'), 'p: 0;m: 0;');
equal(expand('p0!+m0!'), 'p: 0 !important;m: 0 !important;');
});
it('embedded variables', () => {
equal(expand('foo$bar'), 'foo: $bar;');
equal(expand('foo$bar-2'), 'foo: $bar-2;');
equal(expand('foo$bar@bam'), 'foo: $bar @bam;');
});
it('parse value', () => {
const opt: ParseOptions = { value: true };
let prop = parse('${1:foo} ${2:bar}, baz', opt)[0];
equal(prop.name, undefined);
equal(prop.value.length, 2);
equal(prop.value[0].value.length, 2);
equal(prop.value[0].value[0].type, 'Field');
prop = parse('scale3d(${1:x}, ${2:y}, ${3:z})', opt)[0];
const fn = prop.value[0].value[0] as FunctionCall;
equal(prop.value.length, 1);
equal(prop.value[0].value.length, 1);
equal(fn.type, 'FunctionCall');
equal(fn.name, 'scale3d');
prop = parse('repeat(2,auto) / repeat(auto-fit, minmax(250px, 1fr))', opt)[0];
equal(prop.value.length, 1);
});
it('errors', () => {
throws(() => expand('p10 '), /Unexpected token/);
});
});
================================================
FILE: packages/css-abbreviation/test/tokenizer.ts
================================================
import { describe, it } from 'node:test';
import { deepStrictEqual as deepEqual } from 'node:assert';
import tokenize from '../src/tokenizer';
describe('Tokenizer', () => {
it('numeric values', () => {
deepEqual(tokenize('p10'), [
{ type: 'Literal', value: 'p', start: 0, end: 1 },
{ type: 'NumberValue', value: 10, rawValue: '10', unit: '', start: 1, end: 3 }
]);
deepEqual(tokenize('p-10'), [
{ type: 'Literal', value: 'p', start: 0, end: 1 },
{ type: 'NumberValue', value: -10, rawValue: '-10', unit: '', start: 1, end: 4 }
]);
deepEqual(tokenize('p-10-'), [
{ type: 'Literal', value: 'p', start: 0, end: 1 },
{ type: 'NumberValue', value: -10, rawValue: '-10', unit: '', start: 1, end: 4 },
{ type: 'Operator', operator: '-', start: 4, end: 5 }
]);
deepEqual(tokenize('p-10-20'), [
{ type: 'Literal', value: 'p', start: 0, end: 1 },
{ type: 'NumberValue', value: -10, rawValue: '-10', unit: '', start: 1, end: 4 },
{ type: 'Operator', operator: '-', start: 4, end: 5 },
{ type: 'NumberValue', value: 20, rawValue: '20', unit: '', start: 5, end: 7 }
]);
deepEqual(tokenize('p-10--20'), [
{ type: 'Literal', value: 'p', start: 0, end: 1 },
{ type: 'NumberValue', value: -10, rawValue: '-10', unit: '', start: 1, end: 4 },
{ type: 'Operator', operator: '-', start: 4, end: 5 },
{ type: 'NumberValue', value: -20, rawValue: '-20', unit: '', start: 5, end: 8 }
]);
deepEqual(tokenize('p-10-20--30'), [
{ type: 'Literal', value: 'p', start: 0, end: 1 },
{ type: 'NumberValue', value: -10, rawValue: '-10', unit: '', start: 1, end: 4 },
{ type: 'Operator', operator: '-', start: 4, end: 5 },
{ type: 'NumberValue', value: 20, rawValue: '20', unit: '', start: 5, end: 7 },
{ type: 'Operator', operator: '-', start: 7, end: 8 },
{ type: 'NumberValue', value: -30, rawValue: '-30', unit: '', start: 8, end: 11 }
]);
deepEqual(tokenize('p-10p-20--30'), [
{ type: 'Literal', value: 'p', start: 0, end: 1 },
{ type: 'NumberValue', value: -10, rawValue: '-10', unit: 'p', start: 1, end: 5 },
{ type: 'NumberValue', value: -20, rawValue: '-20', unit: '', start: 5, end: 8 },
{ type: 'Operator', operator: '-', start: 8, end: 9 },
{ type: 'NumberValue', value: -30, rawValue: '-30', unit: '', start: 9, end: 12 }
]);
deepEqual(tokenize('p-10%-20--30'), [
{ type: 'Literal', value: 'p', start: 0, end: 1 },
{ type: 'NumberValue', value: -10, rawValue: '-10', unit: '%', start: 1, end: 5 },
{ type: 'NumberValue', value: -20, rawValue: '-20', unit: '', start: 5, end: 8 },
{ type: 'Operator', operator: '-', start: 8, end: 9 },
{ type: 'NumberValue', value: -30, rawValue: '-30', unit: '', start: 9, end: 12 }
]);
});
it('float values', () => {
deepEqual(tokenize('p.5'), [
{ type: 'Literal', value: 'p', start: 0, end: 1 },
{ type: 'NumberValue', value: 0.5, rawValue: '.5', unit: '', start: 1, end: 3 }
]);
deepEqual(tokenize('p-.5'), [
{ type: 'Literal', value: 'p', start: 0, end: 1 },
{ type: 'NumberValue', value: -0.5, rawValue: '-.5', unit: '', start: 1, end: 4 }
]);
deepEqual(tokenize('p.1.2.3'), [
{ type: 'Literal', value: 'p', start: 0, end: 1 },
{ type: 'NumberValue', value: 0.1, rawValue: '.1', unit: '', start: 1, end: 3 },
{ type: 'NumberValue', value: 0.2, rawValue: '.2', unit: '', start: 3, end: 5 },
{ type: 'NumberValue', value: 0.3, rawValue: '.3', unit: '', start: 5, end: 7 }
]);
deepEqual(tokenize('p.1-.2.3'), [
{ type: 'Literal', value: 'p', start: 0, end: 1 },
{ type: 'NumberValue', value: 0.1, rawValue: '.1', unit: '', start: 1, end: 3 },
{ type: 'Operator', operator: '-', start: 3, end: 4 },
{ type: 'NumberValue', value: 0.2, rawValue: '.2', unit: '', start: 4, end: 6 },
{ type: 'NumberValue', value: 0.3, rawValue: '.3', unit: '', start: 6, end: 8 }
]);
deepEqual(tokenize('p.1--.2.3'), [
{ type: 'Literal', value: 'p', start: 0, end: 1 },
{ type: 'NumberValue', value: 0.1, rawValue: '.1', unit: '', start: 1, end: 3 },
{ type: 'Operator', operator: '-', start: 3, end: 4 },
{ type: 'NumberValue', value: -0.2, rawValue: '-.2', unit: '', start: 4, end: 7 },
{ type: 'NumberValue', value: 0.3, rawValue: '.3', unit: '', start: 7, end: 9 }
]);
deepEqual(tokenize('10'), [
{ type: 'NumberValue', value: 10, rawValue: '10', unit: '', start: 0, end: 2 },
]);
deepEqual(tokenize('.1'), [
{ type: 'NumberValue', value: 0.1, rawValue: '.1', unit: '', start: 0, end: 2 },
]);
// NB: now dot should be a part of literal
// throws(() => tokenize('.foo'), /Unexpected character at 1/);
});
it('color values', () => {
deepEqual(tokenize('c#'), [
{ type: 'Literal', value: 'c', start: 0, end: 1 },
{ type: 'ColorValue', r: 0, g: 0, b: 0, a: 1, raw: '', start: 1, end: 2 }
]);
deepEqual(tokenize('c#1'), [
{ type: 'Literal', value: 'c', start: 0, end: 1 },
{ type: 'ColorValue', r: 17, g: 17, b: 17, a: 1, raw: '1', start: 1, end: 3 }
]);
deepEqual(tokenize('c#.'), [
{ type: 'Literal', value: 'c', start: 0, end: 1 },
{ type: 'ColorValue', r: 0, g: 0, b: 0, a: 1, raw: '.', start: 1, end: 3 }
]);
deepEqual(tokenize('c#f'), [
{ type: 'Literal', value: 'c', start: 0, end: 1 },
{ type: 'ColorValue', r: 255, g: 255, b: 255, a: 1, raw: 'f', start: 1, end: 3 }
]);
deepEqual(tokenize('c#a#b#c'), [
{ type: 'Literal', value: 'c', start: 0, end: 1 },
{ type: 'ColorValue', r: 170, g: 170, b: 170, a: 1, raw: 'a', start: 1, end: 3 },
{ type: 'ColorValue', r: 187, g: 187, b: 187, a: 1, raw: 'b', start: 3, end: 5 },
{ type: 'ColorValue', r: 204, g: 204, b: 204, a: 1, raw: 'c', start: 5, end: 7 }
]);
deepEqual(tokenize('c#af'), [
{ type: 'Literal', value: 'c', start: 0, end: 1 },
{ type: 'ColorValue', r: 175, g: 175, b: 175, a: 1, raw: 'af', start: 1, end: 4 }
]);
deepEqual(tokenize('c#fc0'), [
{ type: 'Literal', value: 'c', start: 0, end: 1 },
{ type: 'ColorValue', r: 255, g: 204, b: 0, a: 1, raw: 'fc0', start: 1, end: 5 }
]);
deepEqual(tokenize('c#11.5'), [
{ type: 'Literal', value: 'c', start: 0, end: 1 },
{ type: 'ColorValue', r: 17, g: 17, b: 17, a: 0.5, raw: '11.5', start: 1, end: 6 }
]);
deepEqual(tokenize('c#.99'), [
{ type: 'Literal', value: 'c', start: 0, end: 1 },
{ type: 'ColorValue', r: 0, g: 0, b: 0, a: 0.99, raw: '.99', start: 1, end: 5 }
]);
deepEqual(tokenize('c#t'), [
{ type: 'Literal', value: 'c', start: 0, end: 1 },
{ type: 'ColorValue', r: 0, g: 0, b: 0, a: 0, raw: 't', start: 1, end: 3 }
]);
deepEqual(tokenize('c#${fff}'), [
{ type: 'Literal', value: 'c', start: 0, end: 1 },
{ type: 'Literal', value: '#', start: 1, end: 2 },
{ type: 'Field', index: undefined, name: 'fff', start: 2, end: 8 }
]);
});
it('keywords', () => {
deepEqual(tokenize('m:a'), [
{ type: 'Literal', value: 'm', start: 0, end: 1 },
{ type: 'Operator', operator: ':', start: 1, end: 2 },
{ type: 'Literal', value: 'a', start: 2, end: 3 }
]);
deepEqual(tokenize('m-a'), [
{ type: 'Literal', value: 'm', start: 0, end: 1 },
{ type: 'Operator', operator: '-', start: 1, end: 2 },
{ type: 'Literal', value: 'a', start: 2, end: 3 }
]);
deepEqual(tokenize('m-abc'), [
{ type: 'Literal', value: 'm', start: 0, end: 1 },
{ type: 'Operator', operator: '-', start: 1, end: 2 },
{ type: 'Literal', value: 'abc', start: 2, end: 5 }
]);
deepEqual(tokenize('m-a0'), [
{ type: 'Literal', value: 'm', start: 0, end: 1 },
{ type: 'Operator', operator: '-', start: 1, end: 2 },
{ type: 'Literal', value: 'a', start: 2, end: 3 },
{ type: 'NumberValue', value: 0, rawValue: '0', unit: '', start: 3, end: 4 }
]);
deepEqual(tokenize('m-a0-a'), [
{ type: 'Literal', value: 'm', start: 0, end: 1 },
{ type: 'Operator', operator: '-', start: 1, end: 2 },
{ type: 'Literal', value: 'a', start: 2, end: 3 },
{ type: 'NumberValue', value: 0, rawValue: '0', unit: '', start: 3, end: 4 },
{ type: 'Operator', operator: '-', start: 4, end: 5 },
{ type: 'Literal', value: 'a', start: 5, end: 6 }
]);
});
it('arguments', () => {
deepEqual(tokenize('lg(top, "red, black", rgb(0, 0, 0) 10%)'), [
{ type: 'Literal', value: 'lg', start: 0, end: 2 },
{ type: 'Bracket', open: true, start: 2, end: 3 },
{ type: 'Literal', value: 'top', start: 3, end: 6 },
{ type: 'Operator', operator: ',', start: 6, end: 7 },
{ type: 'WhiteSpace', start: 7, end: 8 },
{ type: 'StringValue', value: 'red, black', quote: 'double', start: 8, end: 20 },
{ type: 'Operator', operator: ',', start: 20, end: 21 },
{ type: 'WhiteSpace', start: 21, end: 22 },
{ type: 'Literal', value: 'rgb', start: 22, end: 25 },
{ type: 'Bracket', open: true, start: 25, end: 26 },
{ type: 'NumberValue', value: 0, rawValue: '0', unit: '', start: 26, end: 27 },
{ type: 'Operator', operator: ',', start: 27, end: 28 },
{ type: 'WhiteSpace', start: 28, end: 29 },
{ type: 'NumberValue', value: 0, rawValue: '0', unit: '', start: 29, end: 30 },
{ type: 'Operator', operator: ',', start: 30, end: 31 },
{ type: 'WhiteSpace', start: 31, end: 32 },
{ type: 'NumberValue', value: 0, rawValue: '0', unit: '', start: 32, end: 33 },
{ type: 'Bracket', open: false, start: 33, end: 34 },
{ type: 'WhiteSpace', start: 34, end: 35 },
{ type: 'NumberValue', value: 10, rawValue: '10', unit: '%', start: 35, end: 38 },
{ type: 'Bracket', open: false, start: 38, end: 39 }
]);
});
it('important', () => {
deepEqual(tokenize('!'), [
{ type: 'Operator', operator: '!', start: 0, end: 1 }
]);
deepEqual(tokenize('p!'), [
{ type: 'Literal', value: 'p', start: 0, end: 1 },
{ type: 'Operator', operator: '!', start: 1, end: 2 }
]);
deepEqual(tokenize('p10!'), [
{ type: 'Literal', value: 'p', start: 0, end: 1 },
{ type: 'NumberValue', value: 10, rawValue: '10', unit: '', start: 1, end: 3 },
{ type: 'Operator', operator: '!', start: 3, end: 4 }
]);
});
it('mixed', () => {
deepEqual(tokenize('bd1-s#fc0'), [
{ type: 'Literal', value: 'bd', start: 0, end: 2 },
{ type: 'NumberValue', value: 1, rawValue: '1', unit: '', start: 2, end: 3 },
{ type: 'Operator', operator: '-', start: 3, end: 4 },
{ type: 'Literal', value: 's', start: 4, end: 5 },
{ type: 'ColorValue', r: 255, g: 204, b: 0, a: 1, raw: 'fc0', start: 5, end: 9 }
]);
deepEqual(tokenize('bd#fc0-1'), [
{ type: 'Literal', value: 'bd', start: 0, end: 2 },
{ type: 'ColorValue', r: 255, g: 204, b: 0, a: 1, raw: 'fc0', start: 2, end: 6 },
{ type: 'Operator', operator: '-', start: 6, end: 7 },
{ type: 'NumberValue', value: 1, rawValue: '1', unit: '', start: 7, end: 8 }
]);
deepEqual(tokenize('p0+m0'), [
{ type: 'Literal', value: 'p', start: 0, end: 1 },
{ type: 'NumberValue', value: 0, rawValue: '0', unit: '', start: 1, end: 2 },
{ type: 'Operator', operator: '+', start: 2, end: 3 },
{ type: 'Literal', value: 'm', start: 3, end: 4 },
{ type: 'NumberValue', value: 0, rawValue: '0', unit: '', start: 4, end: 5 }
]);
deepEqual(tokenize('p0!+m0!'), [
{ type: 'Literal', value: 'p', start: 0, end: 1 },
{ type: 'NumberValue', value: 0, rawValue: '0', unit: '', start: 1, end: 2 },
{ type: 'Operator', operator: '!', start: 2, end: 3 },
{ type: 'Operator', operator: '+', start: 3, end: 4 },
{ type: 'Literal', value: 'm', start: 4, end: 5 },
{ type: 'NumberValue', value: 0, rawValue: '0', unit: '', start: 5, end: 6 },
{ type: 'Operator', operator: '!', start: 6, end: 7 }
]);
deepEqual(tokenize('${2:0}%'), [
{ type: 'Field', index: 2, name: '0', start: 0, end: 6 },
{ type: 'Literal', value: '%', start: 6, end: 7 }
]);
deepEqual(tokenize('.${1:5}'), [
{ type: 'Literal', value: '.', start: 0, end: 1 },
{ type: 'Field', index: 1, name: '5', start: 1, end: 7 },
]);
});
it('embedded variables', () => {
deepEqual(tokenize('foo$bar'), [
{ type: 'Literal', value: 'foo', start: 0, end: 3 },
{ type: 'Literal', value: '$bar', start: 3, end: 7 }
]);
deepEqual(tokenize('foo$bar-2'), [
{ type: 'Literal', value: 'foo', start: 0, end: 3 },
{ type: 'Literal', value: '$bar-2', start: 3, end: 9 }
]);
deepEqual(tokenize('foo$bar@bam'), [
{ type: 'Literal', value: 'foo', start: 0, end: 3 },
{ type: 'Literal', value: '$bar', start: 3, end: 7 },
{ type: 'Literal', value: '@bam', start: 7, end: 11 }
]);
deepEqual(tokenize('@k10'), [
{ type: 'Literal', value: '@k', start: 0, end: 2 },
{ type: 'NumberValue', value: 10, rawValue: '10', unit: '', start: 2, end: 4 }
]);
});
});
================================================
FILE: packages/css-abbreviation/tsconfig.json
================================================
{
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "./dist"
},
"include": ["src"]
}
================================================
FILE: packages/scanner/.gitignore
================================================
/scanner.js
/scanner.es.js
/scanner.cjs
/*.d.ts
/*.map
================================================
FILE: packages/scanner/.npmignore
================================================
npm-debug.log*
node_modules
jspm_packages
.npm
/.*
/*.*
!/scanner.js
!/scanner.cjs
!/*.d.ts
!/*.map
/test
/src
================================================
FILE: packages/scanner/LICENSE
================================================
MIT License
Copyright (c) 2020 Sergey Chikuyonok <serge.che@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: packages/scanner/package.json
================================================
{
"name": "@emmetio/scanner",
"version": "1.0.4",
"description": "Scans given text character-by-character",
"main": "./dist/scanner.cjs",
"module": "./dist/scanner.js",
"types": "./dist/scanner.d.ts",
"type": "module",
"exports": {
"import": "./dist/scanner.js",
"require": "./dist/scanner.cjs"
},
"scripts": {
"test": "tsx --test ./test/*.ts",
"build": "rollup -c",
"watch": "rollup -wc",
"clean": "rm ./scanner.* ./*.d.ts",
"prepublishOnly": "npm test && npm run clean && npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/emmetio/stream-reader.git"
},
"keywords": [
"emmet",
"stream",
"scanner"
],
"author": "Sergey Chikuyonok <serge.che@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/emmetio/emmet/issues"
},
"homepage": "https://github.com/emmetio/emmet#readme"
}
================================================
FILE: packages/scanner/rollup.config.js
================================================
import typescript from '@rollup/plugin-typescript';
export default {
input: './src/scanner.ts',
plugins: [typescript()],
output: [{
format: 'cjs',
exports: 'named',
sourcemap: true,
file: './dist/scanner.cjs'
}, {
format: 'es',
sourcemap: true,
file: './dist/scanner.js'
}]
};
================================================
FILE: packages/scanner/src/scanner.ts
================================================
export * from './utils';
type MatchFn = (ch: number) => boolean;
/**
* A streaming, character code-based string reader
*/
export default class Scanner {
/** Current string */
string: string;
/** Current scanner position */
pos: number;
/** Lower range limit where string reader is available */
start: number;
/** Upper range limit where string reader is available */
end: number;
constructor(str: string, start?: number, end?: number) {
if (end == null && typeof str === 'string') {
end = str.length;
}
this.string = str;
this.pos = this.start = start || 0;
this.end = end || 0;
}
/**
* Returns true only if the stream is at the end of the file.
*/
eof(): boolean {
return this.pos >= this.end;
}
/**
* Creates a new stream instance which is limited to given `start` and `end`
* range. E.g. its `eof()` method will look at `end` property, not actual
* stream end
*/
limit(start?: number, end?: number): Scanner {
return new Scanner(this.string, start, end);
}
/**
* Returns the next character code in the stream without advancing it.
* Will return NaN at the end of the file.
*/
peek(): number {
return this.string.charCodeAt(this.pos);
}
/**
* Returns the next character in the stream and advances it.
* Also returns <code>undefined</code> when no more characters are available.
*/
next(): number | undefined {
if (this.pos < this.string.length) {
return this.string.charCodeAt(this.pos++);
}
}
/**
* `match` can be a character code or a function that takes a character code
* and returns a boolean. If the next character in the stream 'matches'
* the given argument, it is consumed and returned.
* Otherwise, `false` is returned.
*/
eat(match: number | MatchFn): boolean {
const ch = this.peek();
const ok = typeof match === 'function' ? match(ch) : ch === match;
if (ok) {
this.next();
}
return ok;
}
/**
* Repeatedly calls <code>eat</code> with the given argument, until it
* fails. Returns <code>true</code> if any characters were eaten.
*/
eatWhile(match: number | MatchFn): boolean {
const start = this.pos;
while (!this.eof() && this.eat(match)) { /* */ }
return this.pos !== start;
}
/**
* Backs up the stream n characters. Backing it up further than the
* start of the current token will cause things to break, so be careful.
*/
backUp(n: number) {
this.pos -= (n || 1);
}
/**
* Get the string between the start of the current token and the
* current stream position.
*/
current(): string {
return this.substring(this.start, this.pos);
}
/**
* Returns substring for given range
*/
substring(start: number, end?: number): string {
return this.string.slice(start, end);
}
/**
* Creates error object with current stream state
*/
error(message: string, pos = this.pos): ScannerError {
return new ScannerError(`${message} at ${pos + 1}`, pos, this.string);
}
}
export class ScannerError extends Error {
pos: number;
string: string;
constructor(message: string, pos: number, str: string) {
super(message);
this.pos = pos;
this.string = str;
}
}
================================================
FILE: packages/scanner/src/utils.ts
================================================
import type Scanner from './scanner';
interface QuotedOptions {
/** A character code of quote-escape symbol */
escape?: number;
/** Throw error if quotes string can’t be properly consumed */
throws?: boolean;
}
const defaultQuotedOptions: QuotedOptions = {
escape: 92, // \ character
throws: false
};
/**
* Check if given code is a number
*/
export function isNumber(code: number): boolean {
return code > 47 && code < 58;
}
/**
* Check if given character code is alpha code (letter through A to Z)
*/
export function isAlpha(code: number, from?: number, to?: number): boolean {
from = from || 65; // A
to = to || 90; // Z
code &= ~32; // quick hack to convert any char code to uppercase char code
return code >= from && code <= to;
}
/**
* Check if given character code is alpha-numeric (letter through A to Z or number)
*/
export function isAlphaNumeric(code: number): boolean {
return isNumber(code) || isAlpha(code);
}
export function isAlphaNumericWord(code: number): boolean {
return isNumber(code) || isAlphaWord(code);
}
export function isAlphaWord(code: number): boolean {
return code === 95 /* _ */ || isAlpha(code);
}
/**
* Check for Umlauts i.e. ä, Ä, ö, Ö, ü and Ü
*/
export function isUmlaut(code: number): boolean {
return code === 196
|| code == 214
|| code === 220
|| code === 228
|| code === 246
|| code === 252;
}
/**
* Check if given character code is a white-space character: a space character
* or line breaks
*/
export function isWhiteSpace(code: number) {
return code === 32 /* space */
|| code === 9 /* tab */
|| code === 160; /* non-breaking space */
}
/**
* Check if given character code is a space character
*/
export function isSpace(code: number): boolean {
return isWhiteSpace(code)
|| code === 10 /* LF */
|| code === 13; /* CR */
}
/**
* Consumes 'single' or "double"-quoted string from given string, if possible
* @return `true` if quoted string was consumed. The contents of quoted string
* will be available as `stream.current()`
*/
export function eatQuoted(stream: Scanner, options?: QuotedOptions): boolean {
options = { ...defaultQuotedOptions, ...options };
const start = stream.pos;
const quote = stream.peek();
if (stream.eat(isQuote)) {
while (!stream.eof()) {
switch (stream.next()) {
case quote:
stream.start = start;
return true;
case options.escape:
stream.next();
break;
}
}
// If we’re here then stream wasn’t properly consumed.
// Revert stream and decide what to do
stream.pos = start;
if (options.throws) {
throw stream.error('Unable to consume quoted string');
}
}
return false;
}
/**
* Check if given character code is a quote character
*/
export function isQuote(code: number): boolean {
return code === 39 /* ' */ || code === 34 /* " */;
}
/**
* Eats paired characters substring, for example `(foo)` or `[bar]`
* @param open Character code of pair opening
* @param close Character code of pair closing
* @return Returns `true` if character pair was successfully consumed, it’s
* content will be available as `stream.current()`
*/
export function eatPair(stream: Scanner, open: number, close: number, options?: QuotedOptions): boolean {
options = { ...defaultQuotedOptions, ...options };
const start = stream.pos;
if (stream.eat(open)) {
let stack = 1;
let ch: number;
while (!stream.eof()) {
if (eatQuoted(stream, options)) {
continue;
}
ch = stream.next()!;
if (ch === open) {
stack++;
} else if (ch === close) {
stack--;
if (!stack) {
stream.start = start;
return true;
}
} else if (ch === options.escape) {
stream.next();
}
}
// If we’re here then paired character can’t be consumed
stream.pos = start;
if (options.throws) {
throw stream.error(`Unable to find matching pair for ${String.fromCharCode(open)}`);
}
}
return false;
}
================================================
FILE: packages/scanner/test/stream-reader.ts
================================================
import { describe, it } from 'node:test';
import { strictEqual as equal, ok } from 'node:assert';
import StreamReader from '../src/scanner';
describe('Stream Reader', () => {
it('basic', () => {
const data = 'hello';
const s = new StreamReader(data);
equal(s.string, data);
equal(s.start, 0);
equal(s.pos, 0);
equal(s.peek(), data.charCodeAt(0));
equal(s.start, 0);
equal(s.pos, 0);
equal(s.next(), data.charCodeAt(0));
equal(s.next(), data.charCodeAt(1));
equal(s.start, 0);
equal(s.pos, 2);
equal(s.next(), data.charCodeAt(2));
equal(s.start, 0);
equal(s.pos, 3);
equal(s.current(), data.slice(0, 3));
});
it('should limit reader range', () => {
const outer = new StreamReader('foo bar baz');
const inner = outer.limit(4, 7);
ok(outer !== inner);
let outerValue = '';
let innerValue = '';
while (!outer.eof()) {
outerValue += String.fromCharCode(outer.next()!);
}
while (!inner.eof()) {
innerValue += String.fromCharCode(inner.next()!);
}
equal(outerValue, 'foo bar baz');
equal(innerValue, 'bar');
});
});
================================================
FILE: packages/scanner/test/utils.ts
================================================
import { describe, it } from 'node:test';
import { strictEqual as equal, ok, throws } from 'node:assert';
import StreamReader from '../src/scanner';
import { eatPair, eatQuoted } from '../src/utils';
describe('Pairs', () => {
const code = (ch: string) => ch.charCodeAt(0);
it('eat', () => {
const stream = new StreamReader('[foo] (bar (baz) bam)');
ok(eatPair(stream, code('['), code(']')));
equal(stream.start, 0);
equal(stream.pos, 5);
equal(stream.current(), '[foo]');
// No pair here
ok(!eatPair(stream, code('('), code(')'), { throws: true }));
stream.eatWhile(code(' '));
ok(eatPair(stream, code('('), code(')'), { throws: true }));
equal(stream.start, 6);
equal(stream.pos, 21);
equal(stream.current(), '(bar (baz) bam)');
});
it('eat with quotes', () => {
const stream = new StreamReader('[foo "bar]" ]');
ok(eatPair(stream, code('['), code(']')));
equal(stream.start, 0);
equal(stream.pos, 13);
equal(stream.current(), '[foo "bar]" ]');
});
it('handle invalid', () => {
const stream = new StreamReader('[foo');
ok(!eatPair(stream, code('['), code(']')));
equal(stream.start, 0);
equal(stream.pos, 0);
throws(() => ok(!eatPair(stream, code('['), code(']'), { throws: true })),
/Unable to find matching pair/);
});
});
describe('Quoted', () => {
it('eat quoted', () => {
const data = '"foo" \'bar\'';
const stream = new StreamReader(data);
ok(eatQuoted(stream));
equal(stream.start, 0);
equal(stream.pos, 5);
equal(stream.current(), '"foo"');
// no double-quoted value ahead
ok(!eatQuoted(stream, { throws: true }));
// eat space
ok(stream.eatWhile(' '.charCodeAt(0)));
equal(stream.pos, 8);
ok(eatQuoted(stream));
equal(stream.start, 8);
equal(stream.pos, 13);
equal(stream.current(), '\'bar\'');
ok(stream.eof());
});
it('handle broken strings', () => {
const stream = new StreamReader('"foo');
ok(!eatQuoted(stream));
equal(stream.pos, 0);
throws(() => eatQuoted(stream, { throws: true }), /Unable to consume quoted string/);
});
it('handle escapes', () => {
const stream = new StreamReader('"foo\\"bar" baz');
ok(eatQuoted(stream));
equal(stream.start, 0);
equal(stream.pos, 10);
equal(stream.current(), '"foo\\"bar"');
});
});
================================================
FILE: packages/scanner/tsconfig.json
================================================
{
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "./dist"
},
"include": ["src"]
}
================================================
FILE: rollup.config.js
================================================
import { extname } from 'path';
import typescript from '@rollup/plugin-typescript';
import nodeResolve from '@rollup/plugin-node-resolve';
export default {
input: './src/index.ts',
plugins: [nodeResolve(), json(), typescript()],
output: [{
file: './dist/emmet.js',
format: 'es',
sourcemap: true
}, {
file: './dist/emmet.cjs',
format: 'cjs',
exports: 'named',
sourcemap: true
}]
};
function json() {
return {
transform(code, id) {
if (extname(id) === '.json') {
return { code: `export default ${code}`, map: null };
}
}
};
}
================================================
FILE: src/config.ts
================================================
import type { Abbreviation } from '@emmetio/abbreviation';
import markupSnippets from './snippets/html.json' with { type: 'json' };
import stylesheetSnippets from './snippets/css.json' with { type: 'json' };
import xslSnippets from './snippets/xsl.json' with { type: 'json' };
import pugSnippets from './snippets/pug.json' with { type: 'json' };
import variables from './snippets/variables.json' with { type: 'json' };
import type { CSSSnippet } from './stylesheet/snippets.js';
export type SyntaxType = 'markup' | 'stylesheet';
export type FieldOutput = (index: number, placeholder: string, offset: number, line: number, column: number) => string;
export type TextOutput = (text: string, offset: number, line: number, column: number) => string;
export type StringCase = '' | 'lower' | 'upper';
export interface SnippetsMap {
[name: string]: string;
}
export interface AbbreviationContext {
name: string;
attributes?: { [name: string]: string | null };
}
/**
* Raw config which contains per-syntax options. `markup` and `syntax` keys are
* reserved for global settings for all markup and stylesheet syntaxes
*/
export interface GlobalConfig {
[syntax: string]: Partial<BaseConfig>;
}
export interface BaseConfig {
/* Type of abbreviation context, default is `markup` */
type: SyntaxType;
/** Options for abbreviation output */
options: Partial<Options>;
/** Substitutions for variable names */
variables: SnippetsMap;
/** Abbreviation name to snippets mapping */
snippets: SnippetsMap;
}
interface ResolvedConfig extends BaseConfig {
/** Host syntax */
syntax: string;
/**
* Context of abbreviation. For markup abbreviation, it contains parent tag
* name with attributes, for stylesheet abbreviation it contains property name
* if abbreviation is expanded as value
*/
context?: AbbreviationContext;
/** Text to wrap with abbreviation */
text?: string | string[];
/** Max amount of repeated elements (fool proof) */
maxRepeat?: number;
/**
* Object for storing internal cache data to be shared across Emmet methods
* invocation. If provided, Emmet will store compute-intensive data in this
* object and will re-use it during editor session.
* Every time user settings are changed, you should empty cache by passing
* new object.
*/
cache?: Cache;
/**
* A callback for internal warnings or errors (for example, when parsing invalid abbreviation)
*/
warn?: (message: string, err?: Error) => void
}
export type Config = ResolvedConfig & { options: Options };
export type UserConfig = Partial<ResolvedConfig>;
export interface Cache {
stylesheetSnippets?: CSSSnippet[];
markupSnippets?: { [name: string]: Abbreviation | null };
}
export interface Options {
/////////////////////
// Generic options //
/////////////////////
/** A list of inline-level elements */
inlineElements: string[];
////////////////////
// Output options //
////////////////////
/** A string for one level indent */
'output.indent': string;
/**
* A string for base indent, e.g. context indentation which will be added
* for every generated line
*/
'output.baseIndent': string;
/** A string to use as a new line */
'output.newline': string;
/** Tag case: lower, upper or '' (keep as-is) */
'output.tagCase': StringCase;
/** Attribute name case: lower, upper or '' (keep as-is) */
'output.attributeCase': StringCase;
/** Attribute value quotes: 'single' or 'double' */
'output.attributeQuotes': 'single' | 'double';
/** Enable output formatting (indentation and line breaks) */
'output.format': boolean;
/** When enabled, automatically adds inner line breaks for leaf (e.g. without children) nodes */
'output.formatLeafNode': boolean;
/** A list of tag names that should not get inner indentation */
'output.formatSkip': string[];
/** A list of tag names that should *always* get inner indentation. */
'output.formatForce': string[];
/**
* How many inline sibling elements should force line break for each tag.
* Set to `0` to output all inline elements without formatting.
* Set to `1` to output all inline elements with formatting (same as block-level).
*/
'output.inlineBreak': number;
/**
* Produce compact notation of boolean attributes: attributes which doesn’t have value.
* With this option enabled, outputs `<div contenteditable>` instead of
* `<div contenteditable="contenteditable">`
*/
'output.compactBoolean': boolean;
/** A list of boolean attributes */
'output.booleanAttributes': string[];
/** Reverses attribute merging directions when resolving snippets */
'output.reverseAttributes': boolean;
/** Style of self-closing tags: html (`<br>`), xml (`<br/>`) or xhtml (`<br />`) */
'output.selfClosingStyle': 'html' | 'xml' | 'xhtml';
/**
* A function that takes field index and optional placeholder and returns
* a string field (tabstop) for host editor. For example, a TextMate-style
* field is `$index` or `${index:placeholder}`
* @param index Field index
* @param placeholder Field placeholder (default value), if any
* @param offset Current character offset from the beginning of generated content
* @param line Current line of generated output
* @param column Current column in line
*/
'output.field': FieldOutput;
/**
* A function for processing text chunk passed to `OutputStream`.
* May be used by editor for escaping characters, if necessary
*/
'output.text': TextOutput;
////////////////////
// Markup options //
////////////////////
/**
* Automatically update value of <a> element's href attribute
* if inserting URL or email
*/
'markup.href': boolean;
/**
* Attribute name mapping. Can be used to change attribute names for output.
* For example, `class` -> `className` in JSX. If a key ends with `*`, this
* value will be used for multi-attributes: currentry, it’s a `class` and `id`
* since `multiple` marker is added for shorthand attributes only.
* Example: `{ "class*": "styleName" }`
*/
'markup.attributes'?: Record<string, string>;
/**
* Prefixes for attribute values.
* If specified, a value is treated as prefix for object notation and
* automatically converts attribute value into expression if `jsx` is enabled.
* Same as in `markup.attributes` option, a `*` can be used.
*/
'markup.valuePrefix'?: Record<string, string>;
////////////////////////////////
// Element commenting options //
////////////////////////////////
/**
* Enable/disable element commenting: generate comments before open and/or
* after close tag
*/
'comment.enabled': boolean;
/**
* Attributes that should trigger node commenting on specific node,
* if commenting is enabled
*/
'comment.trigger': string[];
/**
* Template string for comment to be placed *before* opening tag
*/
'comment.before': string;
/**
* Template string for comment to be placed *after* closing tag.
* Example: `\n<!-- /[#ID][.CLASS] -->`
*/
'comment.after': string;
/////////////////
// BEM options //
/////////////////
/** Enable/disable BEM addon */
'bem.enabled': boolean;
/** A string for separating elements in output class */
'bem.element': string;
/** A string for separating modifiers in output class */
'bem.modifier': string;
/////////////////
// JSX options //
/////////////////
/** Enable/disable JSX addon */
'jsx.enabled': boolean;
////////////////////////
// Stylesheet options //
////////////////////////
/** List of globally available keywords for properties */
'stylesheet.keywords': string[];
/**
* List of unitless properties, e.g. properties where numeric values without
* explicit unit will be outputted as is, without default value
*/
'stylesheet.unitless': string[];
/** Use short hex notation where possible, e.g. `#000` instead of `#000000` */
'stylesheet.shortHex': boolean;
/** A string between property name and value */
'stylesheet.between': string;
/** A string after property value */
'stylesheet.after': string;
/** A unit suffix to output by default after integer values, 'px' by default */
'stylesheet.intUnit': string;
/** A unit suffix to output by default after float values, 'em' by default */
'stylesheet.floatUnit': string;
/**
* Aliases for custom units in abbreviation. For example, `r: 'rem'` will
* output `10rem` for abbreviation `10r`
*/
'stylesheet.unitAliases': SnippetsMap;
/** Output abbreviation as JSON object properties (for CSS-in-JS syntaxes) */
'stylesheet.json': boolean;
/** Use double quotes for JSON values */
'stylesheet.jsonDoubleQuotes': boolean;
/**
* A float number between 0 and 1 to pick fuzzy-matched abbreviations.
* Lower value will pick more abbreviations (and less accurate)
*/
'stylesheet.fuzzySearchMinScore': number;
/**
* Force strict abbreviation match. If Emmet is unable to match abbreviation
* with existing snippets, it will convert it to CSS property (`false`)
* or skip it (`true`). E.g. `foo-bar` will expand to `foo: bar` if this option
* is disabled or empty string if enabled
*/
'stylesheet.strictMatch': boolean;
}
/**
* Default syntaxes for abbreviation types
*/
export const defaultSyntaxes: { [name in SyntaxType]: string } = {
markup: 'html',
stylesheet: 'css'
};
/**
* List of all known syntaxes
*/
export const syntaxes = {
markup: ['html', 'xml', 'xsl', 'jsx', 'js', 'pug', 'slim', 'haml', 'vue', 'svelte'],
stylesheet: ['css', 'sass', 'scss', 'less', 'sss', 'stylus']
};
export const defaultOptions: Options = {
'inlineElements': [
'a', 'abbr', 'acronym', 'applet', 'b', 'basefont', 'bdo',
'big', 'br', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i',
'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'map', 'object', 'q',
's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup',
'textarea', 'tt', 'u', 'var'
],
'output.indent': '\t',
'output.baseIndent': '',
'output.newline': '\n',
'output.tagCase': '',
'output.attributeCase': '',
'output.attributeQuotes': 'double',
'output.format': true,
'output.formatLeafNode': false,
'output.formatSkip': ['html'],
'output.formatForce': ['body'],
'output.inlineBreak': 3,
'output.compactBoolean': false,
'output.booleanAttributes': [
'contenteditable', 'seamless', 'async', 'autofocus',
'autoplay', 'checked', 'controls', 'defer', 'disabled', 'formnovalidate',
'hidden', 'ismap', 'loop', 'multiple', 'muted', 'novalidate', 'readonly',
'required', 'reversed', 'selected', 'typemustmatch'
],
'output.reverseAttributes': false,
'output.selfClosingStyle': 'html',
'output.field': (index, placeholder) => placeholder,
'output.text': text => text,
'markup.href': true,
'comment.enabled': false,
'comment.trigger': ['id', 'class'],
'comment.before': '',
'comment.after': '\n<!-- /[#ID][.CLASS] -->',
'bem.enabled': false,
'bem.element': '__',
'bem.modifier': '_',
'jsx.enabled': false,
'stylesheet.keywords': ['auto', 'inherit', 'unset', 'none'],
'stylesheet.unitless': ['z-index', 'line-height', 'opacity', 'font-weight', 'zoom', 'flex', 'flex-grow', 'flex-shrink'],
'stylesheet.shortHex': true,
'stylesheet.between': ': ',
'stylesheet.after': ';',
'stylesheet.intUnit': 'px',
'stylesheet.floatUnit': 'em',
'stylesheet.unitAliases': { e: 'em', p: '%', x: 'ex', r: 'rem' },
'stylesheet.json': false,
'stylesheet.jsonDoubleQuotes': false,
'stylesheet.fuzzySearchMinScore': 0,
'stylesheet.strictMatch': false
};
export const defaultConfig: Config = {
type: 'markup',
syntax: 'html',
variables,
snippets: {},
options: defaultOptions
};
/**
* Default per-syntax config
*/
export const syntaxConfig: GlobalConfig = {
markup: {
snippets: parseSnippets(markupSnippets),
},
xhtml: {
options: {
'output.selfClosingStyle': 'xhtml'
}
},
xml: {
options: {
'output.selfClosingStyle': 'xml'
}
},
xsl: {
snippets: parseSnippets(xslSnippets),
options: {
'output.selfClosingStyle': 'xml'
}
},
jsx: {
options: {
'jsx.enabled': true,
'markup.attributes': {
'class': 'className',
'class*': 'styleName',
'for': 'htmlFor'
},
'markup.valuePrefix': {
'class*': 'styles'
}
}
},
vue: {
options: {
'markup.attributes': {
'class*': ':class',
}
}
},
svelte: {
options: {
'jsx.enabled': true
}
},
pug: {
snippets: parseSnippets(pugSnippets)
},
stylesheet: {
snippets: parseSnippets(stylesheetSnippets)
},
sass: {
options: {
'stylesheet.after': ''
}
},
stylus: {
options: {
'stylesheet.between': ' ',
'stylesheet.after': '',
}
}
};
/**
* Parses raw snippets definitions with possibly multiple keys into a plan
* snippet map
*/
export function parseSnippets(snippets: SnippetsMap): SnippetsMap {
const result: SnippetsMap = {};
Object.keys(snippets).forEach(k => {
for (const name of k.split('|')) {
result[name] = snippets[k];
}
});
return result;
}
export default function resolveConfig(config: UserConfig = {}, globals: GlobalConfig = {}): Config {
const type: SyntaxType = config.type || 'markup';
const syntax: string = config.syntax || defaultSyntaxes[type];
return {
...defaultConfig,
...config,
type,
syntax,
variables: mergedData(type, syntax, 'variables', config, globals),
snippets: mergedData(type, syntax, 'snippets', config, globals),
options: mergedData(type, syntax, 'options', config, globals)
};
}
function mergedData<K extends keyof BaseConfig>(type: SyntaxType, syntax: string, key: K, config: UserConfig, globals: GlobalConfig = {}): Config[K] {
const typeDefaults = syntaxConfig[type];
const typeOverride = globals[type];
const syntaxDefaults = syntaxConfig[syntax];
const syntaxOverride = globals[syntax];
return {
...(defaultConfig[key] as object),
...(typeDefaults && typeDefaults[key] as object),
...(syntaxDefaults && syntaxDefaults[key] as object),
...(typeOverride && typeOverride[key] as object),
...(syntaxOverride && syntaxOverride[key] as object),
...(config[key] as object)
} as Config[K];
}
================================================
FILE: src/extract-abbreviation/brackets.ts
================================================
export const enum Brackets {
SquareL = 91,
SquareR = 93,
RoundL = 40,
RoundR = 41,
CurlyL = 123,
CurlyR = 125,
}
export const bracePairs = {
[Brackets.SquareL]: Brackets.SquareR,
[Brackets.RoundL]: Brackets.RoundR,
[Brackets.CurlyL]: Brackets.CurlyR,
};
================================================
FILE: src/extract-abbreviation/index.ts
================================================
import type { SyntaxType } from '../config';
import backwardScanner, { sol, peek, consume, type BackwardScanner } from './reader';
import isAtHTMLTag from './is-html';
import { isQuote } from './quotes';
import { Brackets, bracePairs } from './brackets';
export interface ExtractOptions {
/**
* Allow parser to look ahead of `pos` index for searching of missing
* abbreviation parts. Most editors automatically inserts closing braces for
* `[`, `{` and `(`, which will most likely be right after current caret position.
* So in order to properly expand abbreviation, user must explicitly move
* caret right after auto-inserted braces. With this option enabled, parser
* will search for closing braces right after `pos`. Default is `true`
*/
lookAhead: boolean;
/**
* Type of context syntax of expanded abbreviation.
* In 'stylesheet' syntax, brackets `[]` and `{}` are not supported thus
* not extracted.
*/
type: SyntaxType;
/**
* A string that should precede abbreviation in order to make it successfully
* extracted. If given, the abbreviation will be extracted from the nearest
* `prefix` occurrence.
*/
prefix: string;
}
export interface ExtractedAbbreviation {
/** Extracted abbreviation */
abbreviation: string;
/** Location of abbreviation in input string */
location: number;
/** Start location of matched abbreviation, including prefix */
start: number;
/** End location of extracted abbreviation */
end: number;
}
const code = (ch: string) => ch.charCodeAt(0);
const specialChars = '#.*:$-_!@%^+>/'.split('').map(code);
const defaultOptions: ExtractOptions = {
type: 'markup',
lookAhead: true,
prefix: ''
};
/**
* Extracts Emmet abbreviation from given string.
* The goal of this module is to extract abbreviation from current editor’s line,
* e.g. like this: `<span>.foo[title=bar|]</span>` -> `.foo[title=bar]`, where
* `|` is a current caret position.
* @param line A text line where abbreviation should be expanded
* @param pos Caret position in line. If not given, uses end of line
* @param options Extracting options
*/
export default function extractAbbreviation(line: string, pos: number = line.length, options: Partial<ExtractOptions> = {}): ExtractedAbbreviation | undefined {
// make sure `pos` is within line range
const opt: ExtractOptions = { ...defaultOptions, ...options };
pos = Math.min(line.length, Math.max(0, pos == null ? line.length : pos));
if (opt.lookAhead) {
pos = offsetPastAutoClosed(line, pos, opt);
}
let ch: number;
const start = getStartOffset(line, pos, opt.prefix || '');
if (start === -1) {
return void 0;
}
const scanner = backwardScanner(line, start);
scanner.pos = pos;
const stack: number[] = [];
while (!sol(scanner)) {
ch = peek(scanner);
if (stack.includes(Brackets.CurlyR)) {
if (ch === Brackets.CurlyR) {
stack.push(ch);
scanner.pos--;
continue;
}
if (ch !== Brackets.CurlyL) {
scanner.pos--;
continue;
}
}
if (isCloseBrace(ch, opt.type)) {
stack.push(ch);
} else if (isOpenBrace(ch, opt.type)) {
if (stack.pop() !== bracePairs[ch]) {
// unexpected brace
break;
}
} else if (stack.includes(Brackets.SquareR) || stack.includes(Brackets.CurlyR)) {
// respect all characters inside attribute sets or text nodes
scanner.pos--;
continue;
} else if (isAtHTMLTag(scanner) || !isAbbreviation(ch)) {
break;
}
scanner.pos--;
}
if (!stack.length && scanner.pos !== pos) {
// Found something, remove some invalid symbols from the
// beginning and return abbreviation
const abbreviation = line.slice(scanner.pos, pos).replace(/^[*+>^]+/, '');
return {
abbreviation,
location: pos - abbreviation.length,
start: options.prefix
? start - options.prefix.length
: pos - abbreviation.length,
end: pos
};
}
}
/**
* Returns new `line` index which is right after characters beyound `pos` that
* editor will likely automatically close, e.g. }, ], and quotes
*/
function offsetPastAutoClosed(line: string, pos: number, options: ExtractOptions): number {
// closing quote is allowed only as a next character
if (isQuote(line.charCodeAt(pos))) {
pos++;
}
// offset pointer until non-autoclosed character is found
while (isCloseBrace(line.charCodeAt(pos), options.type)) {
pos++;
}
return pos;
}
/**
* Returns start offset (left limit) in `line` where we should stop looking for
* abbreviation: it’s nearest to `pos` location of `prefix` token
*/
function getStartOffset(line: string, pos: number, prefix: string): number {
if (!prefix) {
return 0;
}
const scanner = backwardScanner(line);
const compiledPrefix = prefix.split('').map(code);
scanner.pos = pos;
let result: number;
while (!sol(scanner)) {
if (consumePair(scanner, Brackets.SquareR, Brackets.SquareL) || consumePair(scanner, Brackets.CurlyR, Brackets.CurlyL)) {
continue;
}
result = scanner.pos;
if (consumeArray(scanner, compiledPrefix)) {
return result;
}
scanner.pos--;
}
return -1;
}
/**
* Consumes full character pair, if possible
*/
function consumePair(scanner: BackwardScanner, close: number, open: number): boolean {
const start = scanner.pos;
if (consume(scanner, close)) {
while (!sol(scanner)) {
if (consume(scanner, open)) {
return true;
}
scanner.pos--;
}
}
scanner.pos = start;
return false;
}
/**
* Consumes all character codes from given array, right-to-left, if possible
*/
function consumeArray(scanner: BackwardScanner, arr: number[]) {
const start = scanner.pos;
let consumed = false;
for (let i = arr.length - 1; i >= 0 && !sol(scanner); i--) {
if (!consume(scanner, arr[i])) {
break;
}
consumed = i === 0;
}
if (!consumed) {
scanner.pos = start;
}
return consumed;
}
function isAbbreviation(ch: number) {
return (ch > 64 && ch < 91) // uppercase letter
|| (ch > 96 && ch < 123) // lowercase letter
|| (ch > 47 && ch < 58) // number
|| specialChars.includes(ch); // special character
}
function isOpenBrace(ch: number, syntax: SyntaxType) {
return ch === Brackets.RoundL || (syntax === 'markup' && (ch === Brackets.SquareL || ch === Brackets.CurlyL));
}
function isCloseBrace(ch: number, syntax: SyntaxType) {
return ch === Brackets.RoundR || (syntax === 'markup' && (ch === Brackets.SquareR || ch === Brackets.CurlyR));
}
================================================
FILE: src/extract-abbreviation/is-html.ts
================================================
import { isQuote, consumeQuoted } from './quotes';
import { type BackwardScanner, consume, sol, consumeWhile, peek } from './reader';
import { Brackets, bracePairs } from './brackets';
const enum Chars {
Tab = 9,
Space = 32,
/** `-` character */
Dash = 45,
/** `/` character */
Slash = 47,
/** `:` character */
Colon = 58,
/** `=` character */
Equals = 61,
/** `<` character */
AngleLeft = 60,
/** `>` character */
AngleRight = 62,
}
/**
* Check if given reader’s current position points at the end of HTML tag
*/
export default function isHtml(scanner: BackwardScanner): boolean {
const start = scanner.pos;
if (!consume(scanner, Chars.AngleRight)) {
return false;
}
let ok = false;
consume(scanner, Chars.Slash); // possibly self-closed element
while (!sol(scanner)) {
consumeWhile(scanner, isWhiteSpace);
if (consumeIdent(scanner)) {
// ate identifier: could be a tag name, boolean attribute or unquoted
// attribute value
if (consume(scanner, Chars.Slash)) {
// either closing tag or invalid tag
ok = consume(scanner, Chars.AngleLeft);
break;
} else if (consume(scanner, Chars.AngleLeft)) {
// opening tag
ok = true;
break;
} else if (consume(scanner, isWhiteSpace)) {
// boolean attribute
continue;
} else if (consume(scanner, Chars.Equals)) {
// simple unquoted value or invalid attribute
if (consumeIdent(scanner)) {
continue;
}
break;
} else if (consumeAttributeWithUnquotedValue(scanner)) {
// identifier was a part of unquoted value
ok = true;
break;
}
// invalid tag
break;
}
if (consumeAttribute(scanner)) {
continue;
}
break;
}
scanner.pos = start;
return ok;
}
/**
* Consumes HTML attribute from given string.
* @return `true` if attribute was consumed.
*/
function consumeAttribute(scanner: BackwardScanner): boolean {
return consumeAttributeWithQuotedValue(scanner) || consumeAttributeWithUnquotedValue(scanner);
}
function consumeAttributeWithQuotedValue(scanner: BackwardScanner): boolean {
const start = scanner.pos;
if (consumeQuoted(scanner) && consume(scanner, Chars.Equals) && consumeIdent(scanner)) {
return true;
}
scanner.pos = start;
return false;
}
function consumeAttributeWithUnquotedValue(scanner: BackwardScanner): boolean {
const start = scanner.pos;
const stack: Brackets[] = [];
while (!sol(scanner)) {
const ch = peek(scanner);
if (isCloseBracket(ch)) {
stack.push(ch);
} else if (isOpenBracket(ch)) {
if (stack.pop() !== bracePairs[ch]) {
// Unexpected open bracket
break;
}
} else if (!isUnquotedValue(ch)) {
break;
}
scanner.pos--;
}
if (start !== scanner.pos && consume(scanner, Chars.Equals) && consumeIdent(scanner)) {
return true;
}
scanner.pos = start;
return false;
}
/**
* Consumes HTML identifier from stream
*/
function consumeIdent(scanner: BackwardScanner): boolean {
return consumeWhile(scanner, isIdent);
}
/**
* Check if given character code belongs to HTML identifier
*/
function isIdent(ch: number): boolean {
return ch === Chars.Colon || ch === Chars.Dash || isAlpha(ch) || isNumber(ch);
}
/**
* Check if given character code is alpha code (letter though A to Z)
*/
function isAlpha(ch: number): boolean {
ch &= ~32; // quick hack to convert any char code to uppercase char code
return ch >= 65 && ch <= 90; // A-Z
}
/**
* Check if given code is a number
*/
function isNumber(ch: number): boolean {
return ch > 47 && ch < 58;
}
/**
* Check if given code is a whitespace
*/
function isWhiteSpace(ch: number): boolean {
return ch === Chars.Space || ch === Chars.Tab;
}
/**
* Check if given code may belong to unquoted attribute value
*/
function isUnquotedValue(ch: number): boolean {
return !isNaN(ch) && ch !== Chars.Equals && !isWhiteSpace(ch) && !isQuote(ch);
}
function isOpenBracket(ch: number): boolean {
return ch === Brackets.CurlyL || ch === Brackets.RoundL || ch === Brackets.SquareL;
}
function isCloseBracket(ch: number): boolean {
return ch === Brackets.CurlyR || ch === Brackets.RoundR || ch === Brackets.SquareR;
}
================================================
FILE: src/extract-abbreviation/quotes.ts
================================================
import { type BackwardScanner, previous, sol, peek } from './reader.js';
const enum Chars {
SingleQuote = 39,
DoubleQuote = 34,
Escape = 92
}
/**
* Check if given character code is a quote
*/
export function isQuote(c?: number) {
return c === Chars.SingleQuote || c === Chars.DoubleQuote;
}
/**
* Consumes quoted value, if possible
* @return Returns `true` is value was consumed
*/
export function consumeQuoted(scanner: BackwardScanner): boolean {
const start = scanner.pos;
const quote = previous(scanner);
if (isQuote(quote)) {
while (!sol(scanner)) {
if (previous(scanner) === quote && peek(scanner) !== Chars.Escape) {
return true;
}
}
}
scanner.pos = start;
return false;
}
================================================
FILE: src/extract-abbreviation/reader.ts
================================================
type Match = ((code: number) => boolean) | number;
export interface BackwardScanner {
/** Text to scan */
text: string;
/** Left bound till given text must be scanned */
start: number;
/** Current scanner position */
pos: number;
}
/**
* Creates structure for scanning given string in backward direction
*/
export default function backwardScanner(text: string, start = 0): BackwardScanner {
return { text, start, pos: text.length };
}
/**
* Check if given scanner position is at start of scanned text
*/
export function sol(scanner: BackwardScanner) {
return scanner.pos === scanner.start;
}
/**
* “Peeks” character code an current scanner location without advancing it
*/
export function peek(scanner: BackwardScanner, offset = 0) {
return scanner.text.charCodeAt(scanner.pos - 1 + offset);
}
/**
* Returns current character code and moves character location one symbol back
*/
export function previous(scanner: BackwardScanner) {
if (!sol(scanner)) {
return scanner.text.charCodeAt(--scanner.pos);
}
}
/**
* Consumes current character code if it matches given `match` code or function
*/
export function consume(scanner: BackwardScanner, match: Match): boolean {
if (sol(scanner)) {
return false;
}
const ok = typeof match === 'function'
? match(peek(scanner))
: match === peek(scanner);
if (ok) {
scanner.pos--;
}
return !!ok;
}
export function consumeWhile(scanner: BackwardScanner, match: Match): boolean {
const start = scanner.pos;
while (consume(scanner, match)) {
// empty
}
return scanner.pos < start;
}
================================================
FILE: src/index.ts
================================================
import markupAbbreviation, { type Abbreviation } from '@emmetio/abbreviation';
import stylesheetAbbreviation, { type CSSAbbreviation } from '@emmetio/css-abbreviation';
import parseMarkup, { stringify as stringifyMarkup } from './markup';
import parseStylesheet, {
stringify as stringifyStylesheet,
convertSnippets as parseStylesheetSnippets,
CSSAbbreviationScope
} from './stylesheet';
import resolveConfig, { type UserConfig, type Config } from './config';
export default function expandAbbreviation(abbr: string, config?: UserConfig): string {
const resolvedConfig = resolveConfig(config);
return resolvedConfig.type === 'stylesheet'
? stylesheet(abbr, resolvedConfig)
: markup(abbr, resolvedConfig);
}
/**
* Expands given *markup* abbreviation (e.g. regular Emmet abbreviation that
* produces structured output like HTML) and outputs it according to options
* provided in config
*/
export function markup(abbr: string | Abbreviation, config: Config) {
return stringifyMarkup(parseMarkup(abbr, config), config);
}
/**
* Expands given *stylesheet* abbreviation (a special Emmet abbreviation designed for
* stylesheet languages like CSS, SASS etc.) and outputs it according to options
* provided in config
*/
export function stylesheet(abbr: string | CSSAbbreviation, config: Config) {
return stringifyStylesheet(parseStylesheet(abbr, config), config);
}
export {
markupAbbreviation, parseMarkup, stringifyMarkup,
stylesheetAbbreviation, parseStylesheet, stringifyStylesheet, parseStylesheetSnippets,
CSSAbbreviationScope
};
export type {
Abbreviation as MarkupAbbreviation, CSSAbbreviation as StylesheetAbbreviation,
};
export { default as extract, type ExtractOptions, type ExtractedAbbreviation } from './extract-abbreviation';
export { default as resolveConfig } from './config';
export type { GlobalConfig, SyntaxType, Config, UserConfig, Options, AbbreviationContext} from './config';
================================================
FILE: src/markup/addon/bem.ts
================================================
import type { AbbreviationNode, Value } from '@emmetio/abbreviation';
import type { Container } from '../utils';
import type { Config, AbbreviationContext } from '../../config';
interface BEMAbbreviationNode extends AbbreviationNode {
_bem?: BEMData;
}
interface BEMAbbreviationContext extends AbbreviationContext {
_bem?: BEMData;
}
interface BEMData {
classNames: string[];
block?: string ;
}
const reElement = /^(-+)([a-z0-9]+[a-z0-9-]*)/i;
const reModifier = /^(_+)([a-z0-9]+[a-z0-9-_]*)/i;
const blockCandidates1 = (className: string) => /^[a-z]\-/i.test(className);
const blockCandidates2 = (className: string) => /^[a-z]/i.test(className);
export default function bem(node: AbbreviationNode, ancestors: Container[], config: Config) {
expandClassNames(node);
expandShortNotation(node, ancestors, config);
}
/**
* Expands existing class names in BEM notation in given `node`.
* For example, if node contains `b__el_mod` class name, this method ensures
* that element contains `b__el` class as well
*/
function expandClassNames(node: BEMAbbreviationNode) {
const data = getBEMData(node);
const classNames: string[] = [];
for (const cl of data.classNames) {
// remove all modifiers and element prefixes from class name to get a base element name
const ix = cl.indexOf('_');
if (ix > 0 && !cl.startsWith('-')) {
classNames.push(cl.slice(0, ix));
classNames.push(cl.slice(ix));
} else {
classNames.push(cl);
}
}
if (classNames.length) {
data.classNames = classNames.filter(uniqueClass);
data.block = findBlockName(data.classNames);
updateClass(node, data.classNames.join(' '));
}
}
/**
* Expands short BEM notation, e.g. `-element` and `_modifier`
*/
function expandShortNotation(node: BEMAbbreviationNode, ancestors: Container[], config: Config) {
const data = getBEMData(node);
const classNames: string[] = [];
const { options } = config;
const path = ancestors.slice(1).concat(node) as BEMAbbreviationNode[];
for (let cl of data.classNames) {
let prefix: string = '';
let m: RegExpMatchArray | null;
const originalClass = cl;
// parse element definition (could be only one)
if (m = cl.match(reElement)) {
prefix = getBlockName(path, m[1].length, config.context) + options['bem.element'] + m[2];
classNames.push(prefix);
cl = cl.slice(m[0].length);
}
// parse modifiers definitions
if (m = cl.match(reModifier)) {
if (!prefix) {
prefix = getBlockName(path, m[1].length);
classNames.push(prefix);
}
classNames.push(`${prefix}${options['bem.modifier']}${m[2]}`);
cl = cl.slice(m[0].length);
}
if (cl === originalClass) {
// class name wasn’t modified: it’s not a BEM-specific class,
// add it as-is into output
classNames.push(originalClass);
}
}
const arrClassNames = classNames.filter(uniqueClass);
if (arrClassNames.length) {
updateClass(node, arrClassNames.join(' '));
}
}
/**
* Returns BEM data from given abbreviation node
*/
function getBEMData(node: BEMAbbreviationNode): BEMData {
if (!node._bem) {
let classValue = '';
if (node.attributes) {
for (const attr of node.attributes) {
if (attr.name === 'class' && attr.value) {
classValue = stringifyValue(attr.value);
break;
}
}
}
node._bem = parseBEM(classValue);
}
return node._bem;
}
function getBEMDataFromContext(context: BEMAbbreviationContext) {
if (!context._bem) {
context._bem = parseBEM(context.attributes && context.attributes.class || '');
}
return context._bem;
}
/**
* Parses BEM data from given class name
*/
function parseBEM(classValue?: string): BEMData {
const classNames = classValue ? classValue.split(/\s+/) : [];
return {
classNames,
block: findBlockName(classNames)
};
}
/**
* Returns block name for given `node` by `prefix`, which tells the depth of
* of parent node lookup
*/
function getBlockName(ancestors: BEMAbbreviationNode[], depth: number = 0, context?: BEMAbbreviationContext): string {
const maxParentIx = 0;
let parentIx = Math.max(ancestors.length - depth, maxParentIx);
do {
const parent = ancestors[parentIx];
if (parent) {
const data = getBEMData(parent as BEMAbbreviationNode);
if (data.block) {
return data.block;
}
}
} while (maxParentIx < parentIx--);
if (context) {
const data = getBEMDataFromContext(context);
if (data.block) {
return data.block;
}
}
return '';
}
function findBlockName(classNames: string[]): string | undefined {
return find(classNames, blockCandidates1)
|| find(classNames, blockCandidates2)
|| void 0;
}
/**
* Finds class name from given list which may be used as block name
*/
function find(classNames: string[], filter: (className: string) => boolean): string | void {
for (const cl of classNames) {
if (reElement.test(cl) || reModifier.test(cl)) {
break;
}
if (filter(cl)) {
return cl;
}
}
}
function updateClass(node: AbbreviationNode, value: string) {
for (const attr of node.attributes!) {
if (attr.name === 'class') {
attr.value = [value];
break;
}
}
}
function stringifyValue(value: Value[]): string {
let result = '';
for (const t of value) {
result += typeof t === 'string' ? t : t.name;
}
return result;
}
function uniqueClass<T>(item: T, ix: number, arr: T[]): boolean {
return !!item && arr.indexOf(item) === ix;
}
================================================
FILE: src/markup/addon/label.ts
================================================
import type { AbbreviationAttribute, AbbreviationNode } from '@emmetio/abbreviation';
import { find } from '../utils';
/**
* Preprocessor of `<label>` element: if it contains `<input>`, remove `for` attribute
* and `id` from input
*/
export default function label(node: AbbreviationNode) {
if (node.name === 'label') {
const input = find(node, n => (n.name === 'input' || n.name === 'textarea'));
if (input) {
// Remove empty `for` attribute
if (node.attributes) {
node.attributes = node.attributes.filter(attr => {
return !(attr.name === 'for' && isEmptyAttribute(attr));
});
}
// Remove empty `id` attribute
if (input.attributes) {
input.attributes = input.attributes.filter(attr => {
return !(attr.name === 'id' && isEmptyAttribute(attr));
});
}
}
}
}
function isEmptyAttribute(attr: AbbreviationAttribute) {
if (!attr.value) {
return true;
}
if (attr.value.length === 1) {
const token = attr.value[0];
if (token && typeof token !== 'string' && !token.name) {
// Attribute contains field
return true;
}
}
return false;
}
================================================
FILE: src/markup/addon/xsl.ts
================================================
import type { AbbreviationNode, AbbreviationAttribute } from '@emmetio/abbreviation';
/**
* XSL transformer: removes `select` attributes from certain nodes that contain
* children
*/
export default function xsl(node: AbbreviationNode) {
if (matchesName(node.name) && node.attributes && (node.children.length || node.value)) {
node.attributes = node.attributes.filter(isAllowed);
}
}
function isAllowed(attr: AbbreviationAttribute): bool
gitextract_u8cxec12/ ├── .editorconfig ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ └── node.js.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── LICENSE ├── README.md ├── lerna.json ├── package.json ├── packages/ │ ├── abbreviation/ │ │ ├── .npmignore │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── rollup.config.js │ │ ├── src/ │ │ │ ├── convert.ts │ │ │ ├── index.ts │ │ │ ├── parser/ │ │ │ │ ├── TokenScanner.ts │ │ │ │ └── index.ts │ │ │ ├── stringify.ts │ │ │ ├── tokenizer/ │ │ │ │ ├── index.ts │ │ │ │ ├── tokens.ts │ │ │ │ └── utils.ts │ │ │ └── types.ts │ │ ├── test/ │ │ │ ├── assets/ │ │ │ │ ├── stringify-node.ts │ │ │ │ └── stringify.ts │ │ │ ├── convert.ts │ │ │ ├── parser.ts │ │ │ └── tokenizer.ts │ │ └── tsconfig.json │ ├── css-abbreviation/ │ │ ├── .npmignore │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── rollup.config.js │ │ ├── src/ │ │ │ ├── index.ts │ │ │ ├── parser/ │ │ │ │ ├── TokenScanner.ts │ │ │ │ └── index.ts │ │ │ └── tokenizer/ │ │ │ ├── index.ts │ │ │ ├── tokens.ts │ │ │ └── utils.ts │ │ ├── test/ │ │ │ ├── assets/ │ │ │ │ └── stringify.ts │ │ │ ├── parser.ts │ │ │ └── tokenizer.ts │ │ └── tsconfig.json │ └── scanner/ │ ├── .gitignore │ ├── .npmignore │ ├── LICENSE │ ├── package.json │ ├── rollup.config.js │ ├── src/ │ │ ├── scanner.ts │ │ └── utils.ts │ ├── test/ │ │ ├── stream-reader.ts │ │ └── utils.ts │ └── tsconfig.json ├── rollup.config.js ├── src/ │ ├── config.ts │ ├── extract-abbreviation/ │ │ ├── brackets.ts │ │ ├── index.ts │ │ ├── is-html.ts │ │ ├── quotes.ts │ │ └── reader.ts │ ├── index.ts │ ├── markup/ │ │ ├── addon/ │ │ │ ├── bem.ts │ │ │ ├── label.ts │ │ │ └── xsl.ts │ │ ├── attributes.ts │ │ ├── format/ │ │ │ ├── comment.ts │ │ │ ├── haml.ts │ │ │ ├── html.ts │ │ │ ├── indent-format.ts │ │ │ ├── pug.ts │ │ │ ├── slim.ts │ │ │ ├── template.ts │ │ │ ├── utils.ts │ │ │ └── walk.ts │ │ ├── implicit-tag.ts │ │ ├── index.ts │ │ ├── lorem/ │ │ │ ├── index.ts │ │ │ ├── latin.json │ │ │ ├── russian.json │ │ │ └── spanish.json │ │ ├── snippets.ts │ │ └── utils.ts │ ├── output-stream.ts │ ├── snippets/ │ │ ├── css.json │ │ ├── html.json │ │ ├── pug.json │ │ ├── variables.json │ │ └── xsl.json │ └── stylesheet/ │ ├── color.ts │ ├── format.ts │ ├── index.ts │ ├── score.ts │ └── snippets.ts ├── test/ │ ├── assets/ │ │ └── stringify.ts │ ├── expand.ts │ ├── extract-abbreviation.ts │ ├── format.ts │ ├── lorem.ts │ ├── markup.ts │ ├── output.ts │ ├── snippets.ts │ └── stylesheet.ts └── tsconfig.json
SYMBOL INDEX (482 symbols across 59 files)
FILE: packages/abbreviation/src/convert.ts
function convert (line 14) | function convert(abbr: TokenGroup, options: ParserOptions = {}): Abbrevi...
function convertStatement (line 76) | function convertStatement(node: TokenStatement, state: ConvertState): Ab...
function convertElement (line 130) | function convertElement(node: TokenElement, state: ConvertState): Abbrev...
function convertGroup (line 168) | function convertGroup(node: TokenGroup, state: ConvertState): Abbreviati...
function convertAttribute (line 181) | function convertAttribute(node: TokenAttribute, state: ConvertState): Ab...
function stringifyName (line 234) | function stringifyName(tokens: ValueToken[], state: ConvertState): string {
function stringifyValue (line 246) | function stringifyValue(tokens: ValueToken[], state: ConvertState): Valu...
function isGroup (line 272) | function isGroup(node: any): node is TokenGroup {
function isField (line 276) | function isField(token: any): token is Field {
function last (line 280) | function last<T>(arr: T[]): T {
function deepestNode (line 284) | function deepestNode(node: AbbreviationNode): AbbreviationNode {
function insertText (line 288) | function insertText(node: AbbreviationNode, text: string) {
function insertHref (line 301) | function insertHref(node: AbbreviationNode, text: string) {
function attachRepeater (line 323) | function attachRepeater(items: AbbreviationNode[], repeater: Repeater): ...
FILE: packages/abbreviation/src/index.ts
type MarkupAbbreviation (line 10) | type MarkupAbbreviation = TokenGroup;
function parseAbbreviation (line 15) | function parseAbbreviation(abbr: string | AllTokens[], options?: ParserO...
FILE: packages/abbreviation/src/parser/TokenScanner.ts
type TokenScanner (line 3) | interface TokenScanner {
type TestFn (line 10) | type TestFn = (token?: AllTokens) => boolean;
function tokenScanner (line 12) | function tokenScanner(tokens: AllTokens[]): TokenScanner {
function peek (line 21) | function peek(scanner: TokenScanner): AllTokens | undefined {
function next (line 25) | function next(scanner: TokenScanner): AllTokens | undefined {
function slice (line 29) | function slice(scanner: TokenScanner, from = scanner.start, to = scanner...
function readable (line 33) | function readable(scanner: TokenScanner): boolean {
function consume (line 37) | function consume(scanner: TokenScanner, test: TestFn): boolean {
function error (line 47) | function error(scanner: TokenScanner, message: string, token = peek(scan...
function consumeWhile (line 58) | function consumeWhile(scanner: TokenScanner, test: TestFn): boolean {
FILE: packages/abbreviation/src/parser/index.ts
type TokenStatement (line 5) | type TokenStatement = TokenElement | TokenGroup;
type TokenAttribute (line 7) | interface TokenAttribute {
type TokenElement (line 18) | interface TokenElement {
type TokenGroup (line 28) | interface TokenGroup {
function abbreviation (line 34) | function abbreviation(abbr: AllTokens[], options: ParserOptions = {}): T...
function statements (line 44) | function statements(scanner: TokenScanner, options: ParserOptions): Toke...
function group (line 80) | function group(scanner: TokenScanner, options: ParserOptions): TokenGrou...
function element (line 94) | function element(scanner: TokenScanner, options: ParserOptions): TokenEl...
function attributeSet (line 139) | function attributeSet(scanner: TokenScanner): TokenAttribute[] | undefin...
function shortAttribute (line 161) | function shortAttribute(scanner: TokenScanner, type: 'class' | 'id', opt...
function attribute (line 197) | function attribute(scanner: TokenScanner): TokenAttribute | undefined {
function repeater (line 218) | function repeater(scanner: TokenScanner): Repeater | undefined {
function quoted (line 227) | function quoted(scanner: TokenScanner): boolean {
function literal (line 248) | function literal(scanner: TokenScanner, allowBrackets?: boolean): boolean {
function elementName (line 295) | function elementName(scanner: TokenScanner, options: ParserOptions): boo...
function text (line 325) | function text(scanner: TokenScanner): boolean {
function getText (line 349) | function getText(scanner: TokenScanner): ValueToken[] {
function isBracket (line 363) | function isBracket(token: AllTokens | undefined, context?: BracketType, ...
function isOperator (line 369) | function isOperator(token: AllTokens | undefined, type?: OperatorType): ...
function isQuote (line 373) | function isQuote(token: AllTokens | undefined, isSingle?: boolean): toke...
function isWhiteSpace (line 377) | function isWhiteSpace(token?: AllTokens): token is WhiteSpace {
function isEquals (line 381) | function isEquals(token: AllTokens) {
function isRepeater (line 385) | function isRepeater(token?: AllTokens): token is Repeater {
function isLiteral (line 389) | function isLiteral(token: AllTokens): token is Literal {
function isCapitalizedLiteral (line 393) | function isCapitalizedLiteral(token: AllTokens) {
function isElementName (line 401) | function isElementName(token: AllTokens): boolean {
function isClassNameOperator (line 405) | function isClassNameOperator(token: AllTokens) {
function isAttributeSetStart (line 409) | function isAttributeSetStart(token?: AllTokens) {
function isAttributeSetEnd (line 413) | function isAttributeSetEnd(token?: AllTokens) {
function isTextStart (line 417) | function isTextStart(token: AllTokens) {
function isGroupStart (line 421) | function isGroupStart(token: AllTokens) {
function createLiteral (line 425) | function createLiteral(value: string): Literal {
function isEmpty (line 429) | function isEmpty(elem: TokenElement): boolean {
function isChildOperator (line 433) | function isChildOperator(token: AllTokens) {
function isSiblingOperator (line 437) | function isSiblingOperator(token: AllTokens) {
function isClimbOperator (line 441) | function isClimbOperator(token: AllTokens) {
function isCloseOperator (line 445) | function isCloseOperator(token: AllTokens) {
FILE: packages/abbreviation/src/stringify.ts
type TokenVisitor (line 4) | type TokenVisitor = (token: Token, state: ConvertState) => string;
method Literal (line 17) | Literal(token: Literal): string {
method Quote (line 20) | Quote(token: Quote) {
method Bracket (line 23) | Bracket(token: Bracket): string {
method Operator (line 32) | Operator(token: Operator) {
method Field (line 35) | Field(token: Field, state) {
method RepeaterPlaceholder (line 48) | RepeaterPlaceholder(token: RepeaterPlaceholder, state) {
method RepeaterNumber (line 61) | RepeaterNumber(token: RepeaterNumber, state) {
method WhiteSpace (line 87) | WhiteSpace(token: WhiteSpace) {
function stringify (line 95) | function stringify(token: ValueToken, state: ConvertState): string {
FILE: packages/abbreviation/src/tokenizer/index.ts
type Context (line 7) | type Context = { [ctx in BracketType]: number } & { quote: number };
function tokenize (line 9) | function tokenize(source: string): AllTokens[] {
function getToken (line 44) | function getToken(scanner: Scanner, ctx: Context): AllTokens | undefined {
function literal (line 59) | function literal(scanner: Scanner, ctx: Context): Literal | undefined {
function whiteSpace (line 130) | function whiteSpace(scanner: Scanner): WhiteSpace | undefined {
function quote (line 145) | function quote(scanner: Scanner): Quote | undefined {
function bracket (line 160) | function bracket(scanner: Scanner): Bracket | undefined {
function operator (line 177) | function operator(scanner: Scanner): Operator | undefined {
function repeater (line 193) | function repeater(scanner: Scanner): Repeater | undefined {
function repeaterPlaceholder (line 220) | function repeaterPlaceholder(scanner: Scanner): RepeaterPlaceholder | un...
function repeaterNumber (line 237) | function repeaterNumber(scanner: Scanner): RepeaterNumber | undefined {
function field (line 272) | function field(scanner: Scanner, ctx: Context): Field | undefined {
function consumePlaceholder (line 310) | function consumePlaceholder(stream: Scanner): string {
function isAllowedOperator (line 339) | function isAllowedOperator(ch: number, ctx: Context): boolean {
function isAllowedSpace (line 354) | function isAllowedSpace(ch: number, ctx: Context): boolean {
function isAllowedRepeater (line 361) | function isAllowedRepeater(ch: number, ctx: Context): boolean {
function bracketType (line 368) | function bracketType(ch: number): BracketType | undefined {
function operatorType (line 385) | function operatorType(ch: number): OperatorType | undefined {
function isOpenBracket (line 399) | function isOpenBracket(ch: number): boolean {
function isElementName (line 408) | function isElementName(ch: number) {
FILE: packages/abbreviation/src/tokenizer/tokens.ts
type OperatorType (line 1) | type OperatorType = 'child' | 'sibling' | 'climb' | 'class' | 'id' | 'cl...
type BracketType (line 2) | type BracketType = 'group' | 'attribute' | 'expression';
type AllTokens (line 4) | type AllTokens =
type NameToken (line 8) | type NameToken = Literal | RepeaterNumber;
type ValueToken (line 9) | type ValueToken = Literal | Quote | Bracket | Field | RepeaterPlaceholde...
type Token (line 11) | interface Token {
type Repeater (line 21) | interface Repeater extends Token {
type RepeaterNumber (line 34) | interface RepeaterNumber extends Token {
type RepeaterPlaceholder (line 50) | interface RepeaterPlaceholder extends Token {
type Field (line 57) | interface Field extends Token {
type Operator (line 63) | interface Operator extends Token {
type Bracket (line 68) | interface Bracket extends Token {
type Quote (line 74) | interface Quote extends Token {
type Literal (line 79) | interface Literal extends Token {
type WhiteSpace (line 84) | interface WhiteSpace extends Token {
FILE: packages/abbreviation/src/tokenizer/utils.ts
type Chars (line 3) | const enum Chars {
function escaped (line 77) | function escaped(scanner: Scanner): boolean {
FILE: packages/abbreviation/src/types.ts
type ParserOptions (line 3) | interface ParserOptions {
type ConvertState (line 20) | interface ConvertState {
type Value (line 33) | type Value = string | Field;
type AttributeType (line 34) | type AttributeType = 'raw' | 'singleQuote' | 'doubleQuote' | 'expression';
type Abbreviation (line 36) | interface Abbreviation {
type AbbreviationNode (line 41) | interface AbbreviationNode {
type AbbreviationAttribute (line 53) | interface AbbreviationAttribute {
FILE: packages/abbreviation/test/assets/stringify-node.ts
function stringify (line 3) | function stringify(abbr: Abbreviation): string {
function elem (line 7) | function elem(node: AbbreviationNode): string {
function attribute (line 21) | function attribute(attr: AbbreviationAttribute): string {
function stringifyValue (line 27) | function stringifyValue(items: Value[]): string {
FILE: packages/abbreviation/test/assets/stringify.ts
type TokenVisitor (line 4) | type TokenVisitor = <T extends AllTokens>(token: T) => string;
type TokenVisitorMap (line 5) | interface TokenVisitorMap {
method Repeater (line 20) | Repeater(token: Repeater) {
method RepeaterNumber (line 23) | RepeaterNumber(token: RepeaterNumber) {
method RepeaterPlaceholder (line 26) | RepeaterPlaceholder() {
method Field (line 29) | Field(node: Field) {
method Operator (line 34) | Operator(node: Operator) {
method Bracket (line 37) | Bracket(node: Bracket) {
method Quote (line 52) | Quote(node: Quote) {
method Literal (line 55) | Literal(node: Literal) {
method WhiteSpace (line 58) | WhiteSpace() {
function statement (line 63) | function statement(node: TokenElement | TokenGroup): string {
function element (line 71) | function element(node: TokenElement): string {
function attribute (line 82) | function attribute(attr: TokenAttribute): string {
function tokenList (line 88) | function tokenList(tokens?: AllTokens[]): string {
function str (line 92) | function str(token: AllTokens): string {
function content (line 100) | function content(node: TokenStatement): string {
function stringify (line 104) | function stringify(abbr: TokenGroup): string {
FILE: packages/abbreviation/test/convert.ts
function parse (line 6) | function parse(abbr: string, options?: ParserOptions) {
FILE: packages/css-abbreviation/src/index.ts
type CSSAbbreviation (line 8) | type CSSAbbreviation = CSSProperty[];
function parse (line 13) | function parse(abbr: string | AllTokens[], options?: ParseOptions): CSSA...
FILE: packages/css-abbreviation/src/parser/TokenScanner.ts
type TokenScanner (line 3) | interface TokenScanner {
type TestFn (line 10) | type TestFn = (token?: AllTokens) => boolean;
function tokenScanner (line 12) | function tokenScanner(tokens: AllTokens[]): TokenScanner {
function peek (line 21) | function peek(scanner: TokenScanner): AllTokens | undefined {
function next (line 25) | function next(scanner: TokenScanner): AllTokens | undefined {
function slice (line 29) | function slice(scanner: TokenScanner, from = scanner.start, to = scanner...
function readable (line 33) | function readable(scanner: TokenScanner): boolean {
function consume (line 37) | function consume(scanner: TokenScanner, test: TestFn): boolean {
function error (line 46) | function error(scanner: TokenScanner, message: string, token = peek(scan...
function consumeWhile (line 57) | function consumeWhile(scanner: TokenScanner, test: TestFn): boolean {
FILE: packages/css-abbreviation/src/parser/index.ts
type Value (line 5) | type Value = StringValue | NumberValue | ColorValue | Literal | Function...
type FunctionCall (line 7) | interface FunctionCall {
type CSSValue (line 13) | interface CSSValue {
type CSSProperty (line 18) | interface CSSProperty {
type ParseOptions (line 26) | interface ParseOptions {
function parser (line 31) | function parser(tokens: AllTokens[], options: ParseOptions = {}): CSSPro...
function consumeProperty (line 50) | function consumeProperty(scanner: TokenScanner, options: ParseOptions): ...
function consumeValue (line 88) | function consumeValue(scanner: TokenScanner, inArgument: boolean): CSSVa...
function consumeArguments (line 119) | function consumeArguments(scanner: TokenScanner): CSSValue[] | undefined {
function isLiteral (line 138) | function isLiteral(token: AllTokens): token is Literal {
function isBracket (line 142) | function isBracket(token: AllTokens, open?: boolean): token is Bracket {
function isOpenBracket (line 146) | function isOpenBracket(token: AllTokens) {
function isCloseBracket (line 150) | function isCloseBracket(token: AllTokens) {
function isWhiteSpace (line 154) | function isWhiteSpace(token: AllTokens): token is WhiteSpace {
function isOperator (line 158) | function isOperator(token: AllTokens, operator?: OperatorType): token is...
function isSiblingOperator (line 162) | function isSiblingOperator(token: AllTokens) {
function isArgumentDelimiter (line 166) | function isArgumentDelimiter(token: AllTokens) {
function isFragmentDelimiter (line 170) | function isFragmentDelimiter(token: AllTokens) {
function isImportant (line 174) | function isImportant(token: AllTokens) {
function isValue (line 178) | function isValue(token: AllTokens): token is StringValue | NumberValue |...
function isValueDelimiter (line 187) | function isValueDelimiter(token: AllTokens): boolean {
function isFunctionStart (line 192) | function isFunctionStart(scanner: TokenScanner): boolean {
FILE: packages/css-abbreviation/src/tokenizer/index.ts
function tokenize (line 8) | function tokenize(abbr: string, isValue?: boolean): AllTokens[] {
function getToken (line 47) | function getToken(scanner: Scanner, short?: boolean) {
function field (line 59) | function field(scanner: Scanner): Field | undefined {
function consumePlaceholder (line 96) | function consumePlaceholder(stream: Scanner): string {
function literal (line 129) | function literal(scanner: Scanner, short?: boolean): Literal | undefined {
function createLiteral (line 151) | function createLiteral(scanner: Scanner, start = scanner.start, end = sc...
function numberValue (line 164) | function numberValue(scanner: Scanner): NumberValue | undefined {
function stringValue (line 187) | function stringValue(scanner: Scanner): StringValue | undefined {
function colorValue (line 218) | function colorValue(scanner: Scanner): ColorValue | Literal | undefined {
function colorAlpha (line 259) | function colorAlpha(scanner: Scanner): string {
function whiteSpace (line 275) | function whiteSpace(scanner: Scanner): WhiteSpace | undefined {
function customProperty (line 289) | function customProperty(scanner: Scanner): CustomProperty | undefined {
function bracket (line 309) | function bracket(scanner: Scanner): Bracket | undefined {
function operator (line 324) | function operator(scanner: Scanner): Operator | undefined {
function consumeNumber (line 340) | function consumeNumber(stream: Scanner): boolean {
function isIdentPrefix (line 366) | function isIdentPrefix(code: number): boolean {
function operatorType (line 373) | function operatorType(ch: number): OperatorType | undefined {
function isHex (line 385) | function isHex(code: number): boolean {
function isKeyword (line 389) | function isKeyword(code: number): boolean {
function isBracket (line 393) | function isBracket(code: number) {
function isLiteral (line 397) | function isLiteral(code: number) {
function parseColor (line 404) | function parseColor(value: string, alpha?: string): { r: number, g: numb...
function shouldConsumeDashAfter (line 451) | function shouldConsumeDashAfter(token: AllTokens): boolean {
function mergeTokens (line 463) | function mergeTokens(scanner: Scanner, tokens: AllTokens[]) {
function last (line 485) | function last<T>(arr: T[]): T | undefined {
FILE: packages/css-abbreviation/src/tokenizer/tokens.ts
type AllTokens (line 1) | type AllTokens = Bracket | Literal | Operator | WhiteSpace | ColorValue
type OperatorType (line 4) | const enum OperatorType {
type Token (line 12) | interface Token {
type Operator (line 22) | interface Operator extends Token {
type Bracket (line 27) | interface Bracket extends Token {
type Literal (line 32) | interface Literal extends Token {
type CustomProperty (line 37) | interface CustomProperty extends Token {
type NumberValue (line 42) | interface NumberValue extends Token {
type ColorValue (line 49) | interface ColorValue extends Token {
type StringValue (line 58) | interface StringValue extends Token {
type WhiteSpace (line 64) | interface WhiteSpace extends Token {
type Field (line 68) | interface Field extends Token {
FILE: packages/css-abbreviation/src/tokenizer/utils.ts
type Chars (line 1) | const enum Chars {
FILE: packages/css-abbreviation/test/assets/stringify.ts
function stringify (line 3) | function stringify(prop: CSSProperty): string {
function stringifyValue (line 7) | function stringifyValue(value: CSSValue): string {
function stringifyToken (line 11) | function stringifyToken(token: Value): string {
function toHex (line 36) | function toHex(num: number): string {
function pad (line 40) | function pad(value: string, len: number): string {
FILE: packages/scanner/src/scanner.ts
type MatchFn (line 3) | type MatchFn = (ch: number) => boolean;
class Scanner (line 8) | class Scanner {
method constructor (line 20) | constructor(str: string, start?: number, end?: number) {
method eof (line 33) | eof(): boolean {
method limit (line 42) | limit(start?: number, end?: number): Scanner {
method peek (line 50) | peek(): number {
method next (line 58) | next(): number | undefined {
method eat (line 70) | eat(match: number | MatchFn): boolean {
method eatWhile (line 85) | eatWhile(match: number | MatchFn): boolean {
method backUp (line 95) | backUp(n: number) {
method current (line 103) | current(): string {
method substring (line 110) | substring(start: number, end?: number): string {
method error (line 117) | error(message: string, pos = this.pos): ScannerError {
class ScannerError (line 122) | class ScannerError extends Error {
method constructor (line 126) | constructor(message: string, pos: number, str: string) {
FILE: packages/scanner/src/utils.ts
type QuotedOptions (line 3) | interface QuotedOptions {
function isNumber (line 19) | function isNumber(code: number): boolean {
function isAlpha (line 26) | function isAlpha(code: number, from?: number, to?: number): boolean {
function isAlphaNumeric (line 37) | function isAlphaNumeric(code: number): boolean {
function isAlphaNumericWord (line 41) | function isAlphaNumericWord(code: number): boolean {
function isAlphaWord (line 45) | function isAlphaWord(code: number): boolean {
function isUmlaut (line 52) | function isUmlaut(code: number): boolean {
function isWhiteSpace (line 65) | function isWhiteSpace(code: number) {
function isSpace (line 74) | function isSpace(code: number): boolean {
function eatQuoted (line 85) | function eatQuoted(stream: Scanner, options?: QuotedOptions): boolean {
function isQuote (line 118) | function isQuote(code: number): boolean {
function eatPair (line 129) | function eatPair(stream: Scanner, open: number, close: number, options?:...
FILE: rollup.config.js
function json (line 20) | function json() {
FILE: src/config.ts
type SyntaxType (line 9) | type SyntaxType = 'markup' | 'stylesheet';
type FieldOutput (line 10) | type FieldOutput = (index: number, placeholder: string, offset: number, ...
type TextOutput (line 11) | type TextOutput = (text: string, offset: number, line: number, column: n...
type StringCase (line 12) | type StringCase = '' | 'lower' | 'upper';
type SnippetsMap (line 13) | interface SnippetsMap {
type AbbreviationContext (line 17) | interface AbbreviationContext {
type GlobalConfig (line 26) | interface GlobalConfig {
type BaseConfig (line 30) | interface BaseConfig {
type ResolvedConfig (line 44) | interface ResolvedConfig extends BaseConfig {
type Config (line 76) | type Config = ResolvedConfig & { options: Options };
type UserConfig (line 77) | type UserConfig = Partial<ResolvedConfig>;
type Cache (line 79) | interface Cache {
type Options (line 84) | interface Options {
function parseSnippets (line 454) | function parseSnippets(snippets: SnippetsMap): SnippetsMap {
function resolveConfig (line 465) | function resolveConfig(config: UserConfig = {}, globals: GlobalConfig = ...
function mergedData (line 480) | function mergedData<K extends keyof BaseConfig>(type: SyntaxType, syntax...
FILE: src/extract-abbreviation/brackets.ts
type Brackets (line 1) | const enum Brackets {
FILE: src/extract-abbreviation/index.ts
type ExtractOptions (line 7) | interface ExtractOptions {
type ExtractedAbbreviation (line 33) | interface ExtractedAbbreviation {
function extractAbbreviation (line 65) | function extractAbbreviation(line: string, pos: number = line.length, op...
function offsetPastAutoClosed (line 137) | function offsetPastAutoClosed(line: string, pos: number, options: Extrac...
function getStartOffset (line 155) | function getStartOffset(line: string, pos: number, prefix: string): numb...
function consumePair (line 184) | function consumePair(scanner: BackwardScanner, close: number, open: numb...
function consumeArray (line 203) | function consumeArray(scanner: BackwardScanner, arr: number[]) {
function isAbbreviation (line 222) | function isAbbreviation(ch: number) {
function isOpenBrace (line 229) | function isOpenBrace(ch: number, syntax: SyntaxType) {
function isCloseBrace (line 233) | function isCloseBrace(ch: number, syntax: SyntaxType) {
FILE: src/extract-abbreviation/is-html.ts
type Chars (line 5) | const enum Chars {
function isHtml (line 25) | function isHtml(scanner: BackwardScanner): boolean {
function consumeAttribute (line 83) | function consumeAttribute(scanner: BackwardScanner): boolean {
function consumeAttributeWithQuotedValue (line 87) | function consumeAttributeWithQuotedValue(scanner: BackwardScanner): bool...
function consumeAttributeWithUnquotedValue (line 97) | function consumeAttributeWithUnquotedValue(scanner: BackwardScanner): bo...
function consumeIdent (line 126) | function consumeIdent(scanner: BackwardScanner): boolean {
function isIdent (line 133) | function isIdent(ch: number): boolean {
function isAlpha (line 140) | function isAlpha(ch: number): boolean {
function isNumber (line 148) | function isNumber(ch: number): boolean {
function isWhiteSpace (line 155) | function isWhiteSpace(ch: number): boolean {
function isUnquotedValue (line 162) | function isUnquotedValue(ch: number): boolean {
function isOpenBracket (line 166) | function isOpenBracket(ch: number): boolean {
function isCloseBracket (line 170) | function isCloseBracket(ch: number): boolean {
FILE: src/extract-abbreviation/quotes.ts
type Chars (line 3) | const enum Chars {
function isQuote (line 12) | function isQuote(c?: number) {
function consumeQuoted (line 20) | function consumeQuoted(scanner: BackwardScanner): boolean {
FILE: src/extract-abbreviation/reader.ts
type Match (line 1) | type Match = ((code: number) => boolean) | number;
type BackwardScanner (line 3) | interface BackwardScanner {
function backwardScanner (line 17) | function backwardScanner(text: string, start = 0): BackwardScanner {
function sol (line 24) | function sol(scanner: BackwardScanner) {
function peek (line 31) | function peek(scanner: BackwardScanner, offset = 0) {
function previous (line 38) | function previous(scanner: BackwardScanner) {
function consume (line 47) | function consume(scanner: BackwardScanner, match: Match): boolean {
function consumeWhile (line 63) | function consumeWhile(scanner: BackwardScanner, match: Match): boolean {
FILE: src/index.ts
function expandAbbreviation (line 11) | function expandAbbreviation(abbr: string, config?: UserConfig): string {
function markup (line 23) | function markup(abbr: string | Abbreviation, config: Config) {
function stylesheet (line 32) | function stylesheet(abbr: string | CSSAbbreviation, config: Config) {
FILE: src/markup/addon/bem.ts
type BEMAbbreviationNode (line 5) | interface BEMAbbreviationNode extends AbbreviationNode {
type BEMAbbreviationContext (line 9) | interface BEMAbbreviationContext extends AbbreviationContext {
type BEMData (line 13) | interface BEMData {
function bem (line 23) | function bem(node: AbbreviationNode, ancestors: Container[], config: Con...
function expandClassNames (line 33) | function expandClassNames(node: BEMAbbreviationNode) {
function expandShortNotation (line 58) | function expandShortNotation(node: BEMAbbreviationNode, ancestors: Conta...
function getBEMData (line 103) | function getBEMData(node: BEMAbbreviationNode): BEMData {
function getBEMDataFromContext (line 121) | function getBEMDataFromContext(context: BEMAbbreviationContext) {
function parseBEM (line 132) | function parseBEM(classValue?: string): BEMData {
function getBlockName (line 144) | function getBlockName(ancestors: BEMAbbreviationNode[], depth: number = ...
function findBlockName (line 167) | function findBlockName(classNames: string[]): string | undefined {
function find (line 176) | function find(classNames: string[], filter: (className: string) => boole...
function updateClass (line 188) | function updateClass(node: AbbreviationNode, value: string) {
function stringifyValue (line 197) | function stringifyValue(value: Value[]): string {
function uniqueClass (line 207) | function uniqueClass<T>(item: T, ix: number, arr: T[]): boolean {
FILE: src/markup/addon/label.ts
function label (line 8) | function label(node: AbbreviationNode) {
function isEmptyAttribute (line 29) | function isEmptyAttribute(attr: AbbreviationAttribute) {
FILE: src/markup/addon/xsl.ts
function xsl (line 7) | function xsl(node: AbbreviationNode) {
function isAllowed (line 13) | function isAllowed(attr: AbbreviationAttribute): boolean {
function matchesName (line 17) | function matchesName(name?: string): boolean {
FILE: src/markup/attributes.ts
function mergeAttributes (line 8) | function mergeAttributes(node: AbbreviationNode, config: Config) {
function mergeValue (line 41) | function mergeValue(prev?: Value[], next?: Value[], glue?: string): Valu...
function mergeDeclarations (line 61) | function mergeDeclarations(dest: AbbreviationAttribute, src: Abbreviatio...
function append (line 84) | function append(tokens: Value[], value: Value) {
FILE: src/markup/format/comment.ts
type CommentWalkState (line 9) | interface CommentWalkState {
function createCommentState (line 16) | function createCommentState(config: Config): CommentWalkState {
function commentNodeBefore (line 29) | function commentNodeBefore(node: AbbreviationNode, state: HTMLWalkState) {
function commentNodeAfter (line 38) | function commentNodeAfter(node: AbbreviationNode, state: HTMLWalkState) {
function shouldComment (line 47) | function shouldComment(node: AbbreviationNode, state: HTMLWalkState): bo...
function output (line 66) | function output(node: AbbreviationNode, tokens: TemplateToken[], state: ...
FILE: src/markup/format/haml.ts
function haml (line 5) | function haml(abbr: Abbreviation, config: Config): string {
FILE: src/markup/format/html.ts
type WalkNext (line 8) | type WalkNext = (node: AbbreviationNode, index: number, items: Abbreviat...
type HTMLWalkState (line 10) | interface HTMLWalkState extends WalkState {
function html (line 21) | function html(abbr: Abbreviation, config: Config): string {
function element (line 35) | function element(node: AbbreviationNode, index: number, items: Abbreviat...
function pushAttribute (line 102) | function pushAttribute(attr: AbbreviationAttribute, state: WalkState) {
function pushSnippet (line 161) | function pushSnippet(node: AbbreviationNode, state: WalkState, next: Wal...
function shouldFormat (line 188) | function shouldFormat(node: AbbreviationNode, index: number, items: Abbr...
function getIndent (line 272) | function getIndent(state: WalkState): number {
function hasNewline (line 285) | function hasNewline(value: Value): boolean {
function startsWithBlockTag (line 292) | function startsWithBlockTag(value: Value[], config: Config): boolean {
function getMultiValue (line 302) | function getMultiValue(key: string, data: Record<string, string>, multip...
function isPropKey (line 306) | function isPropKey(name: string): boolean {
FILE: src/markup/format/indent-format.ts
type AttributesCollection (line 12) | interface AttributesCollection {
type IndentWalkState (line 20) | interface IndentWalkState extends WalkState {
type FormatOptions (line 24) | interface FormatOptions {
function indentFormat (line 53) | function indentFormat(abbr: Abbreviation, config: Config, options?: Part...
function element (line 67) | function element(node: AbbreviationNode, index: number, items: Abbreviat...
function collectAttributes (line 104) | function collectAttributes(node: AbbreviationNode): AttributesCollection {
function pushPrimaryAttributes (line 124) | function pushPrimaryAttributes(attrs: AbbreviationAttribute[], state: Wa...
function pushSecondaryAttributes (line 144) | function pushSecondaryAttributes(attrs: AbbreviationAttribute[], state: ...
function pushValue (line 175) | function pushValue(node: AbbreviationNode, state: IndentWalkState) {
function isPrimaryAttribute (line 220) | function isPrimaryAttribute(attr: AbbreviationAttribute): boolean {
function valueLength (line 227) | function valueLength(tokens: Value[]): number {
function shouldFormat (line 237) | function shouldFormat(node: AbbreviationNode, index: number, items: Abbr...
FILE: src/markup/format/pug.ts
function pug (line 5) | function pug(abbr: Abbreviation, config: Config): string {
FILE: src/markup/format/slim.ts
function slim (line 5) | function slim(abbr: Abbreviation, config: Config): string {
FILE: src/markup/format/template.ts
type TemplateToken (line 1) | type TemplateToken = string | TemplatePlaceholder;
type TemplatePlaceholder (line 2) | interface TemplatePlaceholder {
type TokenScanner (line 8) | interface TokenScanner {
type TemplateChars (line 13) | const enum TemplateChars {
function template (line 35) | function template(text: string): TemplateToken[] {
function consumePlaceholder (line 65) | function consumePlaceholder(scanner: TokenScanner): TemplatePlaceholder ...
function peek (line 99) | function peek(scanner: TokenScanner, pos = scanner.pos): number {
function isTokenStart (line 103) | function isTokenStart(code: number): boolean {
function isToken (line 107) | function isToken(code: number): boolean {
FILE: src/markup/format/utils.ts
function isSnippet (line 11) | function isSnippet(node?: AbbreviationNode): boolean {
function isInlineElement (line 19) | function isInlineElement(node: AbbreviationNode | undefined, config: Con...
function isField (line 26) | function isField(token: Value): token is Field {
function pushTokens (line 30) | function pushTokens(tokens: Value[], state: WalkState) {
function splitByLines (line 54) | function splitByLines(tokens: Value[]): Value[][] {
function shouldOutputAttribute (line 78) | function shouldOutputAttribute(attr: AbbreviationAttribute): boolean {
FILE: src/markup/format/walk.ts
type WalkNext (line 5) | type WalkNext = (node: AbbreviationNode, index: number, items: Abbreviat...
type Visitor (line 6) | type Visitor<S extends WalkState> = (node: AbbreviationNode, index: numb...
type WalkState (line 8) | interface WalkState {
function walk (line 28) | function walk<S extends WalkState>(abbr: Abbreviation, visitor: Visitor<...
function createWalkState (line 47) | function createWalkState(config: Config): WalkState {
FILE: src/markup/implicit-tag.ts
function implicitTag (line 24) | function implicitTag(node: AbbreviationNode, ancestors: Container[], con...
function resolveImplicitTag (line 30) | function resolveImplicitTag(node: AbbreviationNode, ancestors: Container...
function lowercase (line 38) | function lowercase(str?: string): string {
function getParentElement (line 45) | function getParentElement(ancestors: Container[]): AbbreviationNode | un...
FILE: src/markup/index.ts
type Formatter (line 17) | type Formatter = (abbr: Abbreviation, config: Config) => string;
function parse (line 25) | function parse(abbr: string | Abbreviation, config: Config): Abbreviation {
function stringify (line 59) | function stringify(abbr: Abbreviation, config: Config): string {
function transform (line 67) | function transform(node: AbbreviationNode, ancestors: Container[], confi...
FILE: src/markup/lorem/index.ts
type LoremVocabulary (line 9) | interface LoremVocabulary {
function lorem (line 17) | function lorem(node: AbbreviationNode, ancestors: Container[], config: C...
function rand (line 38) | function rand(from: number, to: number): number {
function sample (line 42) | function sample(arr: string[], count: number): string[] {
function choice (line 57) | function choice(val: string): string {
function sentence (line 61) | function sentence(words: string[], end?: string): string {
function capitalize (line 69) | function capitalize(word: string): string {
function insertCommas (line 77) | function insertCommas(words: string[]): string[] {
function paragraph (line 111) | function paragraph(dict: LoremVocabulary, wordCount: number, startWithCo...
function findRepeater (line 131) | function findRepeater(ancestors: Container[]): Repeater | void {
FILE: src/markup/snippets.ts
function resolveSnippets (line 15) | function resolveSnippets(abbr: Abbreviation, config: Config): Abbreviati...
function walkResolve (line 60) | function walkResolve(node: Container, resolve: (node: AbbreviationNode) ...
function mergeNodes (line 84) | function mergeNodes(from: AbbreviationNode, to: AbbreviationNode) {
FILE: src/markup/utils.ts
type Container (line 3) | type Container = Abbreviation | AbbreviationNode;
type WalkVisitor (line 4) | type WalkVisitor<S> = (node: AbbreviationNode, ancestors: Container[], s...
function walk (line 12) | function walk<S>(node: Container, fn: WalkVisitor<S>, state?: S) {
function find (line 27) | function find(node: Container, callback: (node: AbbreviationNode) => boo...
function findDeepest (line 44) | function findDeepest(node: Container): { node: Container, parent?: Conta...
function isNode (line 54) | function isNode(node: Container): node is AbbreviationNode {
FILE: src/output-stream.ts
type OutputStream (line 4) | interface OutputStream {
function createOutputStream (line 16) | function createOutputStream(options: Options, level = 0): OutputStream {
function push (line 30) | function push(stream: OutputStream, text: string) {
function pushString (line 38) | function pushString(stream: OutputStream, value: string) {
function pushNewline (line 54) | function pushNewline(stream: OutputStream, indent?: boolean | number) {
function pushIndent (line 68) | function pushIndent(stream: OutputStream, size = stream.level) {
function pushField (line 76) | function pushField(stream: OutputStream, index: number, placeholder: str...
function tagName (line 85) | function tagName(name: string, config: Config) {
function attrName (line 92) | function attrName(name: string, config: Config) {
function attrQuote (line 99) | function attrQuote(attr: AbbreviationAttribute, config: Config, isOpen?:...
function isBooleanAttribute (line 110) | function isBooleanAttribute(attr: AbbreviationAttribute, config: Config)...
function selfClose (line 118) | function selfClose(config: Config): string {
function isInline (line 130) | function isInline(node: string | AbbreviationNode, config: Config): bool...
function splitByLines (line 142) | function splitByLines(text: string): string[] {
function _push (line 149) | function _push(stream: OutputStream, text: string) {
function strCase (line 155) | function strCase(str: string, type: StringCase) {
FILE: src/stylesheet/color.ts
function color (line 3) | function color(token: ColorValue, shortHex?: boolean): string {
function asHex (line 17) | function asHex(token: ColorValue, short?: boolean): string {
function asRGB (line 27) | function asRGB(token: ColorValue): string {
function frac (line 36) | function frac(num: number, digits = 4): string {
function isShortHex (line 40) | function isShortHex(hex: number): boolean {
function toShortHex (line 44) | function toShortHex(num: number): string {
function toHex (line 48) | function toHex(num: number): string {
function pad (line 52) | function pad(value: string, len: number): string {
FILE: src/stylesheet/format.ts
function css (line 17) | function css(abbr: CSSAbbreviation, config: Config): string {
function property (line 39) | function property(node: CSSProperty, out: OutputStream, config: Config) {
function propertyValue (line 71) | function propertyValue(node: CSSProperty, out: OutputStream, config: Con...
function outputImportant (line 92) | function outputImportant(node: CSSProperty, out: OutputStream, separator...
function outputValue (line 101) | function outputValue(value: CSSValue, out: OutputStream, config: Config) {
function outputToken (line 115) | function outputToken(token: Value, out: OutputStream, config: Config) {
function getSingleNumeric (line 142) | function getSingleNumeric(node: CSSProperty): NumberValue | void {
function toCamelCase (line 154) | function toCamelCase(str: string): string {
function getQuote (line 158) | function getQuote(config: Config): string {
FILE: src/stylesheet/index.ts
type MatchInput (line 12) | type MatchInput = CSSSnippet | string;
function parse (line 20) | function parse(abbr: string | CSSAbbreviation, config: Config): CSSAbbre...
function convertSnippets (line 47) | function convertSnippets(snippets: SnippetsMap): CSSSnippet[] {
function resolveNode (line 60) | function resolveNode(node: CSSProperty, snippets: CSSSnippet[], config: ...
function resolveGradient (line 98) | function resolveGradient(node: CSSProperty, config: Config): boolean {
function resolveAsProperty (line 136) | function resolveAsProperty(node: CSSProperty, snippet: CSSSnippetPropert...
function resolveValueKeywords (line 177) | function resolveValueKeywords(node: CSSProperty, config: Config, snippet...
function resolveAsSnippet (line 206) | function resolveAsSnippet(node: CSSProperty, snippet: CSSSnippetRaw): CS...
function findBestMatch (line 244) | function findBestMatch<T extends MatchInput>(abbr: string, items: T[], m...
function getScoringPart (line 265) | function getScoringPart(item: MatchInput): string {
function getUnmatchedPart (line 274) | function getUnmatchedPart(abbr: string, str: string): string {
function resolveKeyword (line 290) | function resolveKeyword(kw: string, config: Config, snippet?: CSSSnippet...
function resolveNumericValue (line 315) | function resolveNumericValue(node: CSSProperty, config: Config) {
function cssValue (line 337) | function cssValue(...args: Value[]): CSSValue {
function literal (line 347) | function literal(value: string): Literal {
function field (line 354) | function field(index: number, name: string): Field {
function hasField (line 361) | function hasField(value: CSSValue): boolean {
type WrapState (line 371) | interface WrapState {
function wrapWithField (line 378) | function wrapWithField(node: CSSValue, config: Config, state: WrapState ...
function isValueScope (line 416) | function isValueScope(config: Config): boolean {
function getSnippetsForScope (line 427) | function getSnippetsForScope(snippets: CSSSnippet[], config: Config): CS...
FILE: src/stylesheet/score.ts
function scoreMatch (line 11) | function scoreMatch(str1: string, str2: string, partialMatch = false) {
function sum (line 89) | function sum(n: number): number {
FILE: src/stylesheet/snippets.ts
type CSSSnippet (line 4) | type CSSSnippet = CSSSnippetRaw | CSSSnippetProperty;
type KeywordMap (line 6) | interface KeywordMap {
type CSSSnippetType (line 10) | const enum CSSSnippetType {
type CSSSnippetBase (line 15) | interface CSSSnippetBase {
type CSSSnippetRaw (line 20) | interface CSSSnippetRaw extends CSSSnippetBase {
type CSSSnippetProperty (line 25) | interface CSSSnippetProperty extends CSSSnippetBase {
function createSnippet (line 39) | function createSnippet(key: string, value: string): CSSSnippet {
function nest (line 71) | function nest(snippets: CSSSnippet[]): CSSSnippet[] {
function snippetsSort (line 108) | function snippetsSort(a: CSSSnippet, b: CSSSnippet): number {
function parseValue (line 116) | function parseValue(value: string): CSSValue[] {
function isProperty (line 120) | function isProperty(snippet: CSSSnippet): snippet is CSSSnippetProperty {
function collectKeywords (line 124) | function collectKeywords(cssVal: CSSValue, dest: KeywordMap) {
FILE: test/assets/stringify.ts
function stringify (line 3) | function stringify(abbr: Abbreviation): string {
function elem (line 7) | function elem(node: AbbreviationNode): string {
function attribute (line 25) | function attribute(attr: AbbreviationAttribute): string {
function stringifyValue (line 38) | function stringifyValue(items: Value[]): string {
FILE: test/extract-abbreviation.ts
function extract (line 8) | function extract(abbr: string, options?: Partial<ExtractOptions>) {
function result (line 19) | function result(abbreviation: string, location: number, start = location...
FILE: test/format.ts
function createProfile (line 18) | function createProfile(options: Partial<Options>) {
FILE: test/lorem.ts
function wordCount (line 5) | function wordCount(str: string): number {
function splitLines (line 9) | function splitLines(str: string): string[] {
FILE: test/markup.ts
function expand (line 9) | function expand(abbr: string, config = defaultConfig): string {
FILE: test/stylesheet.ts
function expand (line 21) | function expand(abbr: string, config = defaultConfig) {
Condensed preview — 105 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (374K chars).
[
{
"path": ".editorconfig",
"chars": 254,
"preview": "root = true\n\n[*]\nindent_style = space\nindent_size = 4\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ni"
},
{
"path": ".github/FUNDING.yml",
"chars": 598,
"preview": "# These are supported funding model platforms\n\ngithub: emmetio\npatreon: # Replace with a single Patreon username\nopen_co"
},
{
"path": ".github/workflows/node.js.yml",
"chars": 853,
"preview": "# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tes"
},
{
"path": ".gitignore",
"chars": 951,
"preview": "# 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# Directo"
},
{
"path": ".npmignore",
"chars": 77,
"preview": "npm-debug.log*\nnode_modules\njspm_packages\n.npm\n/.*\n/*.*\n/test\n/src\n/packages\n"
},
{
"path": ".npmrc",
"chars": 16,
"preview": "save-exact=true\n"
},
{
"path": "LICENSE",
"chars": 1096,
"preview": "MIT License\n\nCopyright (c) 2020 Sergey Chikuyonok <serge.che@gmail.com>\n\nPermission is hereby granted, free of charge, t"
},
{
"path": "README.md",
"chars": 7578,
"preview": "# Emmet — the essential toolkit for web-developers\n\nEmmet is a web-developer’s toolkit for boosting HTML & CSS code writ"
},
{
"path": "lerna.json",
"chars": 92,
"preview": "{\n \"$schema\": \"node_modules/lerna/schemas/lerna-schema.json\",\n \"version\": \"independent\"\n}\n"
},
{
"path": "package.json",
"chars": 1552,
"preview": "{\n \"name\": \"emmet\",\n \"version\": \"2.4.11\",\n \"description\": \"Emmet — the essential toolkit for web-developers\",\n \"main"
},
{
"path": "packages/abbreviation/.npmignore",
"chars": 67,
"preview": "npm-debug.log*\nnode_modules\njspm_packages\n.npm\n/.*\n/*.*\n/test\n/src\n"
},
{
"path": "packages/abbreviation/LICENSE",
"chars": 1096,
"preview": "MIT License\n\nCopyright (c) 2020 Sergey Chikuyonok <serge.che@gmail.com>\n\nPermission is hereby granted, free of charge, t"
},
{
"path": "packages/abbreviation/README.md",
"chars": 3625,
"preview": "# Emmet markup abbreviation parser\n\nParses given Emmet *markup* abbreviation into AST. Parsing is performed in two steps"
},
{
"path": "packages/abbreviation/package.json",
"chars": 977,
"preview": "{\n \"name\": \"@emmetio/abbreviation\",\n \"version\": \"2.3.3\",\n \"description\": \"Emmet standalone abbreviation parser\",\n \"m"
},
{
"path": "packages/abbreviation/rollup.config.js",
"chars": 384,
"preview": "import typescript from '@rollup/plugin-typescript';\n\nexport default {\n input: './src/index.ts',\n external: ['@emme"
},
{
"path": "packages/abbreviation/src/convert.ts",
"chars": 10374,
"preview": "import { isQuote, isBracket } from './parser';\nimport type { TokenGroup, TokenStatement, TokenElement, TokenAttribute } "
},
{
"path": "packages/abbreviation/src/index.ts",
"chars": 883,
"preview": "import { ScannerError } from '@emmetio/scanner';\nimport parse, { type TokenGroup } from './parser';\nimport tokenize, { g"
},
{
"path": "packages/abbreviation/src/parser/TokenScanner.ts",
"chars": 1525,
"preview": "import type { AllTokens } from '../tokenizer';\n\nexport interface TokenScanner {\n tokens: AllTokens[];\n start: numb"
},
{
"path": "packages/abbreviation/src/parser/index.ts",
"chars": 12976,
"preview": "import type { NameToken, ValueToken, Repeater, AllTokens, BracketType, Bracket, Operator, OperatorType, Quote, WhiteSpac"
},
{
"path": "packages/abbreviation/src/stringify.ts",
"chars": 3186,
"preview": "import type { Token, Literal, Bracket, Field, RepeaterPlaceholder, Repeater, RepeaterNumber, ValueToken, Quote, Operator"
},
{
"path": "packages/abbreviation/src/tokenizer/index.ts",
"chars": 11400,
"preview": "import Scanner, { isSpace, isQuote, isNumber, isAlpha, isAlphaNumericWord, isUmlaut } from '@emmetio/scanner';\nimport ty"
},
{
"path": "packages/abbreviation/src/tokenizer/tokens.ts",
"chars": 2100,
"preview": "export type OperatorType = 'child' | 'sibling' | 'climb' | 'class' | 'id' | 'close' | 'equal';\nexport type BracketType ="
},
{
"path": "packages/abbreviation/src/tokenizer/utils.ts",
"chars": 1458,
"preview": "import type Scanner from '@emmetio/scanner';\n\nexport const enum Chars {\n /** `{` character */\n CurlyBracketOpen = "
},
{
"path": "packages/abbreviation/src/types.ts",
"chars": 1928,
"preview": "import type { Field, Repeater } from './tokenizer';\n\nexport interface ParserOptions {\n /** Text strings to insert int"
},
{
"path": "packages/abbreviation/test/assets/stringify-node.ts",
"chars": 1222,
"preview": "import type { Abbreviation, AbbreviationNode, Value, AbbreviationAttribute } from '../../src';\n\nexport default function "
},
{
"path": "packages/abbreviation/test/assets/stringify.ts",
"chars": 3007,
"preview": "import type { AllTokens, Repeater, RepeaterNumber, Field, OperatorType, Operator, Bracket, Quote, Literal } from '../../"
},
{
"path": "packages/abbreviation/test/convert.ts",
"chars": 6646,
"preview": "import { describe, it } from 'node:test';\nimport { equal } from 'node:assert';\nimport parser, { type ParserOptions } fro"
},
{
"path": "packages/abbreviation/test/parser.ts",
"chars": 7849,
"preview": "import { describe, it } from 'node:test';\nimport { strictEqual as equal, throws } from 'node:assert';\nimport parser from"
},
{
"path": "packages/abbreviation/test/tokenizer.ts",
"chars": 4339,
"preview": "import { describe, it } from 'node:test';\nimport { deepStrictEqual } from 'node:assert';\nimport tokenize from '../src/to"
},
{
"path": "packages/abbreviation/tsconfig.json",
"chars": 115,
"preview": "{\n \"extends\": \"../../tsconfig\",\n \"compilerOptions\": {\n \"outDir\": \"./dist\"\n },\n \"include\": [\"src/**/*.ts\"]\n}\n"
},
{
"path": "packages/css-abbreviation/.npmignore",
"chars": 67,
"preview": "npm-debug.log*\nnode_modules\njspm_packages\n.npm\n/.*\n/*.*\n/test\n/src\n"
},
{
"path": "packages/css-abbreviation/LICENSE",
"chars": 1096,
"preview": "MIT License\n\nCopyright (c) 2020 Sergey Chikuyonok <serge.che@gmail.com>\n\nPermission is hereby granted, free of charge, t"
},
{
"path": "packages/css-abbreviation/README.md",
"chars": 1565,
"preview": "# Emmet stylesheet abbreviation parser\n\nParses given Emmet *stylesheet* abbreviation into AST. Parsing is performed in t"
},
{
"path": "packages/css-abbreviation/package.json",
"chars": 952,
"preview": "{\n \"name\": \"@emmetio/css-abbreviation\",\n \"version\": \"2.1.8\",\n \"description\": \"Parses Emmet CSS abbreviation into AST "
},
{
"path": "packages/css-abbreviation/rollup.config.js",
"chars": 428,
"preview": "import typescript from '@rollup/plugin-typescript';\n\n/** @type {import('rollup').RollupOptions} */\nexport default {\n "
},
{
"path": "packages/css-abbreviation/src/index.ts",
"chars": 891,
"preview": "import { ScannerError } from '@emmetio/scanner';\nimport tokenize, { getToken, type AllTokens } from './tokenizer';\nimpor"
},
{
"path": "packages/css-abbreviation/src/parser/TokenScanner.ts",
"chars": 1500,
"preview": "import type { AllTokens } from '../tokenizer/index.js';\n\nexport interface TokenScanner {\n tokens: AllTokens[];\n st"
},
{
"path": "packages/css-abbreviation/src/parser/index.ts",
"chars": 5991,
"preview": "import { OperatorType } from '../tokenizer/tokens.js';\nimport type { StringValue, NumberValue, ColorValue, Literal, AllT"
},
{
"path": "packages/css-abbreviation/src/tokenizer/index.ts",
"chars": 13632,
"preview": "import { default as Scanner, isAlphaWord, isAlpha, isNumber, isAlphaNumericWord, isSpace, isQuote } from '@emmetio/scann"
},
{
"path": "packages/css-abbreviation/src/tokenizer/tokens.ts",
"chars": 1405,
"preview": "export type AllTokens = Bracket | Literal | Operator | WhiteSpace | ColorValue\n | NumberValue | StringValue | CustomP"
},
{
"path": "packages/css-abbreviation/src/tokenizer/utils.ts",
"chars": 894,
"preview": "export const enum Chars {\n /** `#` character */\n Hash = 35,\n\n /** `$` character */\n Dollar = 36,\n\n /** `-"
},
{
"path": "packages/css-abbreviation/test/assets/stringify.ts",
"chars": 1353,
"preview": "import type { CSSProperty, CSSValue, Value } from '../../src/parser';\n\nexport default function stringify(prop: CSSProper"
},
{
"path": "packages/css-abbreviation/test/parser.ts",
"chars": 3253,
"preview": "import { describe, it } from 'node:test';\nimport { strictEqual as equal, throws } from 'node:assert';\nimport parser, { P"
},
{
"path": "packages/css-abbreviation/test/tokenizer.ts",
"chars": 14671,
"preview": "import { describe, it } from 'node:test';\nimport { deepStrictEqual as deepEqual } from 'node:assert';\nimport tokenize fr"
},
{
"path": "packages/css-abbreviation/tsconfig.json",
"chars": 107,
"preview": "{\n \"extends\": \"../../tsconfig\",\n \"compilerOptions\": {\n \"outDir\": \"./dist\"\n },\n \"include\": [\"src\"]\n}\n"
},
{
"path": "packages/scanner/.gitignore",
"chars": 55,
"preview": "/scanner.js\n/scanner.es.js\n/scanner.cjs\n/*.d.ts\n/*.map\n"
},
{
"path": "packages/scanner/.npmignore",
"chars": 111,
"preview": "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",
"chars": 1096,
"preview": "MIT License\n\nCopyright (c) 2020 Sergey Chikuyonok <serge.che@gmail.com>\n\nPermission is hereby granted, free of charge, t"
},
{
"path": "packages/scanner/package.json",
"chars": 911,
"preview": "{\n \"name\": \"@emmetio/scanner\",\n \"version\": \"1.0.4\",\n \"description\": \"Scans given text character-by-character\",\n \"mai"
},
{
"path": "packages/scanner/rollup.config.js",
"chars": 354,
"preview": "import typescript from '@rollup/plugin-typescript';\n\nexport default {\n input: './src/scanner.ts',\n plugins: [types"
},
{
"path": "packages/scanner/src/scanner.ts",
"chars": 3533,
"preview": "export * from './utils';\n\ntype MatchFn = (ch: number) => boolean;\n\n/**\n * A streaming, character code-based string reade"
},
{
"path": "packages/scanner/src/utils.ts",
"chars": 4437,
"preview": "import type Scanner from './scanner';\n\ninterface QuotedOptions {\n /** A character code of quote-escape symbol */\n "
},
{
"path": "packages/scanner/test/stream-reader.ts",
"chars": 1277,
"preview": "import { describe, it } from 'node:test';\nimport { strictEqual as equal, ok } from 'node:assert';\nimport StreamReader fr"
},
{
"path": "packages/scanner/test/utils.ts",
"chars": 2597,
"preview": "import { describe, it } from 'node:test';\nimport { strictEqual as equal, ok, throws } from 'node:assert';\nimport StreamR"
},
{
"path": "packages/scanner/tsconfig.json",
"chars": 107,
"preview": "{\n \"extends\": \"../../tsconfig\",\n \"compilerOptions\": {\n \"outDir\": \"./dist\"\n },\n \"include\": [\"src\"]\n}\n"
},
{
"path": "rollup.config.js",
"chars": 666,
"preview": "import { extname } from 'path';\nimport typescript from '@rollup/plugin-typescript';\nimport nodeResolve from '@rollup/plu"
},
{
"path": "src/config.ts",
"chars": 15334,
"preview": "import type { Abbreviation } from '@emmetio/abbreviation';\nimport markupSnippets from './snippets/html.json' with { type"
},
{
"path": "src/extract-abbreviation/brackets.ts",
"chars": 291,
"preview": "export const enum Brackets {\n SquareL = 91,\n SquareR = 93,\n RoundL = 40,\n RoundR = 41,\n CurlyL = 123,\n "
},
{
"path": "src/extract-abbreviation/index.ts",
"chars": 7115,
"preview": "import type { SyntaxType } from '../config';\nimport backwardScanner, { sol, peek, consume, type BackwardScanner } from '"
},
{
"path": "src/extract-abbreviation/is-html.ts",
"chars": 4702,
"preview": "import { isQuote, consumeQuoted } from './quotes';\nimport { type BackwardScanner, consume, sol, consumeWhile, peek } fro"
},
{
"path": "src/extract-abbreviation/quotes.ts",
"chars": 786,
"preview": "import { type BackwardScanner, previous, sol, peek } from './reader.js';\n\nconst enum Chars {\n SingleQuote = 39,\n D"
},
{
"path": "src/extract-abbreviation/reader.ts",
"chars": 1668,
"preview": "type Match = ((code: number) => boolean) | number;\n\nexport interface BackwardScanner {\n /** Text to scan */\n text:"
},
{
"path": "src/index.ts",
"chars": 1964,
"preview": "import markupAbbreviation, { type Abbreviation } from '@emmetio/abbreviation';\nimport stylesheetAbbreviation, { type CSS"
},
{
"path": "src/markup/addon/bem.ts",
"chars": 6019,
"preview": "import type { AbbreviationNode, Value } from '@emmetio/abbreviation';\nimport type { Container } from '../utils';\nimport "
},
{
"path": "src/markup/addon/label.ts",
"chars": 1316,
"preview": "import type { AbbreviationAttribute, AbbreviationNode } from '@emmetio/abbreviation';\nimport { find } from '../utils';\n\n"
},
{
"path": "src/markup/addon/xsl.ts",
"chars": 615,
"preview": "import type { AbbreviationNode, AbbreviationAttribute } from '@emmetio/abbreviation';\n\n/**\n * XSL transformer: removes `"
},
{
"path": "src/markup/attributes.ts",
"chars": 2501,
"preview": "import type { AbbreviationAttribute, AbbreviationNode, Value } from '@emmetio/abbreviation';\nimport type { Config } from"
},
{
"path": "src/markup/format/comment.ts",
"chars": 2594,
"preview": "import type { AbbreviationNode, Value } from '@emmetio/abbreviation';\nimport { pushString } from '../../output-stream';\n"
},
{
"path": "src/markup/format/haml.ts",
"chars": 467,
"preview": "import type { Abbreviation } from '@emmetio/abbreviation';\nimport indentFormat from './indent-format';\nimport type { Con"
},
{
"path": "src/markup/format/html.ts",
"chars": 10516,
"preview": "import type { Abbreviation, AbbreviationNode, AbbreviationAttribute, Value } from '@emmetio/abbreviation';\nimport { push"
},
{
"path": "src/markup/format/indent-format.ts",
"chars": 7891,
"preview": "import type { AbbreviationNode, AbbreviationAttribute, Value, Abbreviation } from '@emmetio/abbreviation';\nimport { push"
},
{
"path": "src/markup/format/pug.ts",
"chars": 472,
"preview": "import type { Abbreviation } from '@emmetio/abbreviation';\nimport indentFormat from './indent-format';\nimport type { Con"
},
{
"path": "src/markup/format/slim.ts",
"chars": 384,
"preview": "import type { Abbreviation } from '@emmetio/abbreviation';\nimport indentFormat from './indent-format';\nimport type { Con"
},
{
"path": "src/markup/format/template.ts",
"chars": 3227,
"preview": "export type TemplateToken = string | TemplatePlaceholder;\nexport interface TemplatePlaceholder {\n before: string;\n "
},
{
"path": "src/markup/format/utils.ts",
"chars": 2452,
"preview": "import type { AbbreviationNode, Field, Value, AbbreviationAttribute } from '@emmetio/abbreviation';\nimport type { WalkSt"
},
{
"path": "src/markup/format/walk.ts",
"chars": 1827,
"preview": "import type { AbbreviationNode, Abbreviation } from '@emmetio/abbreviation';\nimport createOutputStream, { type OutputStr"
},
{
"path": "src/markup/implicit-tag.ts",
"chars": 1509,
"preview": "import type { AbbreviationNode } from '@emmetio/abbreviation';\nimport { isNode, type Container } from './utils';\nimport "
},
{
"path": "src/markup/index.ts",
"chars": 2604,
"preview": "import abbreviation from '@emmetio/abbreviation';\nimport type { Abbreviation, AbbreviationNode, ParserOptions } from '@e"
},
{
"path": "src/markup/lorem/index.ts",
"chars": 4200,
"preview": "import type { AbbreviationNode, Repeater } from '@emmetio/abbreviation';\nimport type { Container } from '../utils';\nimpo"
},
{
"path": "src/markup/lorem/latin.json",
"chars": 2061,
"preview": "{\n\t\"common\": [\"lorem\", \"ipsum\", \"dolor\", \"sit\", \"amet\", \"consectetur\", \"adipisicing\", \"elit\"],\n\t\"words\": [\"exercitatione"
},
{
"path": "src/markup/lorem/russian.json",
"chars": 1986,
"preview": "{\n\t\"common\": [\"далеко-далеко\", \"за\", \"словесными\", \"горами\", \"в стране\", \"гласных\", \"и согласных\", \"живут\", \"рыбные\", \"т"
},
{
"path": "src/markup/lorem/spanish.json",
"chars": 2153,
"preview": "{\n\t\"common\": [\"mujer\", \"uno\", \"dolor\", \"más\", \"de\", \"poder\", \"mismo\", \"si\"],\n\t\"words\": [\"ejercicio\", \"preferencia\", \"per"
},
{
"path": "src/markup/snippets.ts",
"chars": 3438,
"preview": "import parse from '@emmetio/abbreviation';\nimport type { AbbreviationNode, AbbreviationAttribute, Abbreviation } from '@"
},
{
"path": "src/markup/utils.ts",
"chars": 1751,
"preview": "import type { Abbreviation, AbbreviationNode } from '@emmetio/abbreviation';\n\nexport type Container = Abbreviation | Abb"
},
{
"path": "src/output-stream.ts",
"chars": 4688,
"preview": "import type { AbbreviationAttribute, AbbreviationNode } from '@emmetio/abbreviation';\nimport type { Config, Options, Str"
},
{
"path": "src/snippets/css.json",
"chars": 11908,
"preview": "{\n\t\"@f\": \"@font-face {\\n\\tfont-family: ${1};\\n\\tsrc: url(${2});\\n}\",\n\t\"@ff\": \"@font-face {\\n\\tfont-family: '${1:FontName"
},
{
"path": "src/snippets/html.json",
"chars": 5965,
"preview": "{\n\t\"a\": \"a[href]\",\n\t\"a:blank\": \"a[href='http://${0}' target='_blank' rel='noopener noreferrer']\",\n\t\"a:link\": \"a[href='ht"
},
{
"path": "src/snippets/pug.json",
"chars": 29,
"preview": "{\n\t\"!!!\": \"{doctype html}\"\n}\n"
},
{
"path": "src/snippets/variables.json",
"chars": 99,
"preview": "{\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",
"chars": 1439,
"preview": "{\n \"tm|tmatch\": \"xsl:template[match mode]\",\n \"tn|tname\": \"xsl:template[name]\",\n \"call\": \"xsl:call-template[name"
},
{
"path": "src/stylesheet/color.ts",
"chars": 1515,
"preview": "import type { ColorValue } from '@emmetio/css-abbreviation';\n\nexport default function color(token: ColorValue, shortHex?"
},
{
"path": "src/stylesheet/format.ts",
"chars": 5533,
"preview": "import type { CSSAbbreviation, CSSProperty, Value, CSSValue, NumberValue } from '@emmetio/css-abbreviation';\nimport crea"
},
{
"path": "src/stylesheet/index.ts",
"chars": 14059,
"preview": "import abbreviation from '@emmetio/css-abbreviation';\nimport type { CSSAbbreviation, CSSProperty, CSSValue, Literal, Val"
},
{
"path": "src/stylesheet/score.ts",
"chars": 2702,
"preview": "/**\n * Calculates how close `str1` matches `str2` using fuzzy match.\n * How matching works:\n * – first characters of bot"
},
{
"path": "src/stylesheet/snippets.ts",
"chars": 3965,
"preview": "import parse from '@emmetio/css-abbreviation';\nimport type { CSSValue, ParseOptions, FunctionCall, Literal } from '@emme"
},
{
"path": "test/assets/stringify.ts",
"chars": 1437,
"preview": "import { Abbreviation, AbbreviationNode, Value, AbbreviationAttribute } from '@emmetio/abbreviation';\n\nexport default fu"
},
{
"path": "test/expand.ts",
"chars": 11260,
"preview": "import { describe, it } from 'node:test';\nimport { strictEqual as equal } from 'node:assert';\nimport expand, { resolveCo"
},
{
"path": "test/extract-abbreviation.ts",
"chars": 5422,
"preview": "import { describe, it } from 'node:test';\nimport { deepStrictEqual, strictEqual, ok } from 'node:assert';\nimport extract"
},
{
"path": "test/format.ts",
"chars": 15768,
"preview": "import { describe, it } from 'node:test';\nimport { equal } from 'node:assert';\nimport html from '../src/markup/format/ht"
},
{
"path": "test/lorem.ts",
"chars": 1989,
"preview": "import { describe, it } from 'node:test';\nimport { ok, strictEqual as equal } from 'node:assert';\nimport expand from '.."
},
{
"path": "test/markup.ts",
"chars": 4949,
"preview": "import { describe, it } from 'node:test';\nimport { strictEqual as equal } from 'node:assert';\nimport parse from '../src/"
},
{
"path": "test/output.ts",
"chars": 3132,
"preview": "import { describe, it } from 'node:test';\nimport { strictEqual as equal } from 'node:assert';\nimport createStream, { pus"
},
{
"path": "test/snippets.ts",
"chars": 907,
"preview": "import { describe, it } from 'node:test';\nimport { ok, strictEqual as equal } from 'node:assert';\nimport markup from '@e"
},
{
"path": "test/stylesheet.ts",
"chars": 11531,
"preview": "import { describe, it } from 'node:test';\nimport { strictEqual as equal, ok } from 'node:assert';\nimport { stylesheet as"
},
{
"path": "tsconfig.json",
"chars": 463,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"es2017\",\n \"sourceMap\": true,\n \"noUnusedLocals\": true,\n \"alwaysStrict\": "
}
]
About this extraction
This page contains the full source code of the emmetio/emmet GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 105 files (343.5 KB), approximately 94.4k tokens, and a symbol index with 482 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.