├── .gitattributes ├── .gitignore ├── .npmignore ├── spec ├── .eslintrc ├── snippet-solver-spec.js └── selector-solver-spec.js ├── test.html ├── keymaps └── selector-to-tag.cson ├── lib ├── utils.js ├── main.js └── selector-solver.js ├── .eslintrc ├── CHANGELOG.md ├── package.json ├── .github └── workflows │ └── tests.yml ├── LICENSE.md └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.gif binary 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*.log 2 | node_modules 3 | /.Trash-1000/ 4 | .DS_Store 5 | Thumbs.db 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.Trash-1000/ 3 | .DS_Store 4 | Thumbs.db 5 | *.log 6 | -------------------------------------------------------------------------------- /spec/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "../.eslintrc" 4 | ], 5 | 6 | "env": { 7 | "jasmine": true 8 | }, 9 | 10 | "globals": { 11 | "waitsForPromise": false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /keymaps/selector-to-tag.cson: -------------------------------------------------------------------------------- 1 | # Keybindings require three things to be fully defined: A selector that is 2 | # matched against the focused element, the keystroke and the command to 3 | # execute. 4 | # 5 | # Below is a basic keybinding which registers on all platforms by applying to 6 | # the root workspace element. 7 | 8 | # For more detailed documentation see 9 | # https://atom.io/docs/latest/advanced/keymaps 10 | 'atom-workspace atom-text-editor:not([mini])': 11 | 'tab': 'selector-to-tag:solve-selector' 12 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | // String extensions 2 | 3 | if (!String.prototype.insertAt) { 4 | String.prototype.insertAt = function (index, string) { 5 | return this.substr(0, index) + string + this.substr(index); 6 | }; 7 | } 8 | 9 | // RegExp extensions 10 | 11 | if (!RegExp.prototype.getAllMatches) { 12 | RegExp.prototype.getAllMatches = function (string) { 13 | var result = []; 14 | var match; 15 | do { 16 | match = this.exec(string); 17 | if (match) { 18 | result.push(match[0]); 19 | } 20 | } while (match); 21 | 22 | return result; 23 | }; 24 | } 25 | 26 | module.exports = {}; 27 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:prettier/recommended" 5 | ], 6 | 7 | "parserOptions": { 8 | "ecmaVersion": 6, 9 | "sourceType": "module", 10 | "ecmaFeatures": { 11 | "impliedStrict": true 12 | } 13 | }, 14 | 15 | "env": { 16 | "node": true, 17 | "browser": true, 18 | "es6": true 19 | }, 20 | 21 | "globals": { 22 | "atom": false, 23 | "com": false 24 | }, 25 | 26 | "rules": { 27 | "eqeqeq": "error", 28 | "block-scoped-var": "error", 29 | "no-mixed-spaces-and-tabs": ["error", "smart-tabs"], 30 | "semi": "error", 31 | "no-console": ["error", {"allow": ["error", "info"]}], 32 | "prettier/prettier": ["error", { 33 | "useTabs": true 34 | }] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.4.1 2 | 3 | - Fixed: prevent expending to div from nothing 4 | 5 | ## 0.4.0 6 | 7 | - Added: now you can use only the id or class to expand to a div with that class and/or id 8 | - Fixed: not expanding if tag ends in dot (for example "div.") 9 | 10 | ## 0.3.0 11 | 12 | - Added option to expand block tags to multiple lines. (thanks @dsandstrom) 13 | 14 | ## 0.2.1 15 | 16 | - Fixed bug where starting a tag with dash (`-`) will crash the package 17 | - Removed menu entry from Packages 18 | 19 | ## 0.2.0 20 | 21 | - Added option to close self-closing tags 22 | 23 | ## 0.1.2 24 | 25 | - Added support for dashes inside selectors 26 | - Fixed deprecated code 27 | 28 | ## 0.1.1 29 | 30 | - Small cosmetic to the docs 31 | 32 | ## 0.1.0 33 | 34 | - Initial release 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "selector-to-tag", 3 | "main": "./lib/main", 4 | "version": "0.6.0", 5 | "description": "Create HTML tag elements using CSS selectors in HTML files", 6 | "repository": "https://github.com/surdu/selector-to-tag", 7 | "license": "MIT", 8 | "engines": { 9 | "atom": ">0.50.0" 10 | }, 11 | "scripts": { 12 | "test": "npx eslint **/*.js" 13 | }, 14 | "keywords": [ 15 | "html", 16 | "css", 17 | "web development", 18 | "solve tags" 19 | ], 20 | "devDependencies": { 21 | "eslint": "^5.0.1", 22 | "eslint-config-prettier": "^6.12.0", 23 | "eslint-plugin-prettier": "^3.1.4", 24 | "prettier": "^2.1.2" 25 | }, 26 | "consumedServices": { 27 | "snippets": { 28 | "versions": { 29 | "0.1.0": "consumeSnippets" 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CI: true 11 | 12 | jobs: 13 | Test: 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, macos-latest, windows-latest] 17 | channel: [stable, beta] 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - uses: actions/checkout@v1 21 | 22 | - uses: UziTech/action-setup-atom@v1 23 | with: 24 | channel: ${{ matrix.channel }} 25 | 26 | - name: Atom version 27 | run: atom -v 28 | 29 | - name: APM version 30 | run: apm -v 31 | 32 | - name: Install dependencies 33 | run: apm install 34 | 35 | - name: Run linter & spellcheck 36 | run: npm test 37 | 38 | - name: Run tests 39 | run: atom --test spec 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015 Nicolae Surdu 2 | All rights reserved. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | const SelectorSolver = require("./selector-solver"); 2 | 3 | module.exports = { 4 | config: { 5 | solveTagsOnly: { 6 | type: "boolean", 7 | default: true, 8 | title: "Solve Plain Tags", 9 | description: 10 | "Solve to tag even if the selector is only the tag with no id and/or class.", 11 | }, 12 | 13 | activeForFileTypes: { 14 | type: "array", 15 | default: ["htm", "html", "kit", "shtml", "tmpl", "tpl", "xhtml"], 16 | title: "File Extensions", 17 | description: "Active for these file extensions", 18 | items: { 19 | type: "string", 20 | }, 21 | }, 22 | 23 | activeForGrammar: { 24 | type: "boolean", 25 | default: false, 26 | title: "Only On HTML Grammar", 27 | description: 28 | "Active only if HTML grammar is selected. The `File Extensions` option will be ignored if this is enabled.", 29 | }, 30 | 31 | closeSelfclosingTags: { 32 | type: "boolean", 33 | default: false, 34 | title: "Close Self-Closing Tags", 35 | description: "Add a backslash before the end of self-closing tags", 36 | }, 37 | 38 | expandBlockTags: { 39 | type: "boolean", 40 | default: false, 41 | title: "Expand Block Tags To Multiple Lines", 42 | description: "Put end tag on a new line", 43 | }, 44 | 45 | blockTags: { 46 | type: "array", 47 | default: [ 48 | "address", 49 | "article", 50 | "aside", 51 | "audio", 52 | "blockquote", 53 | "canvas", 54 | "dd", 55 | "div", 56 | "dl", 57 | "fieldset", 58 | "figcaption", 59 | "figure", 60 | "footer", 61 | "form", 62 | "header", 63 | "hgroup", 64 | "hr", 65 | "main", 66 | "nav", 67 | "noscript", 68 | "ol", 69 | "output", 70 | "p", 71 | "pre", 72 | "section", 73 | "table", 74 | "tfoot", 75 | "ul", 76 | "video", 77 | ], 78 | title: "Block-Level Elements", 79 | description: "HTML tags that will be expanded to multiple lines.", 80 | }, 81 | }, 82 | 83 | availableAPI: {}, 84 | 85 | activate() { 86 | new SelectorSolver(this.availableAPI); 87 | }, 88 | 89 | consumeSnippets(snippetsAPI) { 90 | this.availableAPI.snippetsAPI = snippetsAPI; 91 | }, 92 | }; 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Selector to Tag 2 | 3 | [![Actions Status](https://github.com/surdu/selector-to-tag/workflows/Tests/badge.svg)](https://github.com/surdu/selector-to-tag/actions) 4 | [![Buy me a coffee](https://raw.githubusercontent.com/surdu/surdu/master/ko-fi.svg)](https://ko-fi.com/surdu) 5 | 6 | Selector to Tag is an [Atom editor](https://atom.io/) package that allows you to create HTML tag elements using CSS selectors in HTML files. 7 | 8 | Just type a CSS selector and press TAB : 9 | 10 | ![Demo animation](https://cloud.githubusercontent.com/assets/11520795/6700058/1b18986a-cd11-11e4-9d6a-848b808197c6.gif) 11 | 12 | ## Supported selectors 13 | 14 | For now the following selectors are possible: 15 | 16 | Selector | Output 17 | ------------------|------ 18 | `foo` | `` (when `Solve Plain Tags` option is enabled (default `true`)) 19 | `foo#bar` | `` 20 | `#foo` | `
` 21 | `.foo` | `
` 22 | `foo.bar` | `` 23 | `foo.bar.baz` | `` 24 | `foo#bar.baz` | `` 25 | `foo#bar.baz.qux` | `` 26 | 27 | ## Options 28 | 29 | - **File Extensions** - comma separated file extensions in which this package should be active *(Default: "htm, html, kit, shtml, tmpl, tpl, xhtml")* 30 | 31 | - **Only On HTML Grammar** - extension solves tags in any file that have the HTML grammar active. `File extensions` option will be ignored if this is enabled *(Default: false)* 32 | 33 | - **Solve Plain Tags** - this will indicate if this package should also solve to tags when there is no id or class specified in the selector. *(Default: true)* 34 | 35 | - **Close Self-Closing Tags** - Add a backslash before the end of self-closing tags. For example `` will be solved to `` *(Default: false)* 36 | 37 | - **Expand Block Tags To Multiple Lines** - Puts the cursor and end tag on new lines. *(Default: false)* 38 | 39 | - **Block-Level Elements** - If "Expand block tags to multiple lines" is checked, these tags will count as block tags. *(Default: address, article, aside, audio, blockquote, canvas, dd, div, dl, fieldset, figcaption, figure, footer, form, header, hgroup, hr, main, nav, noscript, ol, output, p, pre, section, table, tfoot, ul, video)* 40 | 41 | ## Support 42 | 43 | If you have any sugestions for other selectors or sugestions in general, please submit an issue on GitHub. 44 | -------------------------------------------------------------------------------- /spec/snippet-solver-spec.js: -------------------------------------------------------------------------------- 1 | describe("Snippet solver", function () { 2 | var editor; 3 | var editorView; 4 | 5 | beforeEach(function () { 6 | waitsForPromise(function () { 7 | return atom.workspace.open("test.html"); 8 | }); 9 | 10 | waitsForPromise(function () { 11 | const selectorToTag = atom.packages.activatePackage("selector-to-tag"); 12 | const snippets = atom.packages.activatePackage("snippets"); 13 | return Promise.all([selectorToTag, snippets]); 14 | }); 15 | 16 | atom.config.unset("selector-to-tag.closeSelfclosingTags"); 17 | atom.config.unset("selector-to-tag.expandBlockTags"); 18 | 19 | runs(function () { 20 | editor = atom.workspace.getActiveTextEditor(); 21 | editor.setText(""); 22 | editorView = atom.views.getView(editor); 23 | }); 24 | }); 25 | 26 | it("should solve inline tags", function () { 27 | editor.setText("link"); 28 | editor.moveToEndOfLine(); 29 | 30 | atom.commands.dispatch(editorView, "selector-to-tag:solve-selector"); 31 | 32 | expect(editor.getText()).toBe(""); 33 | 34 | expect(editor.getCursorScreenPosition()).toEqual([0, 5]); 35 | }); 36 | 37 | it("should solve block tags", function () { 38 | editor.setText("div"); 39 | editor.moveToEndOfLine(); 40 | 41 | atom.commands.dispatch(editorView, "selector-to-tag:solve-selector"); 42 | 43 | expect(editor.getText()).toBe("
"); 44 | expect(editor.getCursorScreenPosition()).toEqual([0, 4]); 45 | 46 | atom.commands.dispatch(editorView, "snippets:next-tab-stop"); 47 | 48 | expect(editor.getCursorScreenPosition()).toEqual([0, 5]); 49 | }); 50 | 51 | it("should solve expanded block tags", function () { 52 | atom.config.set("selector-to-tag.expandBlockTags", true); 53 | 54 | editor.setText("div"); 55 | editor.moveToEndOfLine(); 56 | 57 | atom.commands.dispatch(editorView, "selector-to-tag:solve-selector"); 58 | 59 | expect(editor.getText()).toBe("
\n \n
"); 60 | expect(editor.getCursorScreenPosition()).toEqual([0, 4]); 61 | 62 | atom.commands.dispatch(editorView, "snippets:next-tab-stop"); 63 | 64 | expect(editor.getCursorScreenPosition()).toEqual([1, 2]); 65 | }); 66 | 67 | it("should respect indent style", function () { 68 | atom.config.set("selector-to-tag.expandBlockTags", true); 69 | atom.config.set("editor.tabType", "hard"); 70 | atom.config.set("editor.tabLength", 4); 71 | 72 | editor.setText("div"); 73 | editor.moveToEndOfLine(); 74 | 75 | atom.commands.dispatch(editorView, "selector-to-tag:solve-selector"); 76 | 77 | expect(editor.getText()).toBe("
\n\t\n
"); 78 | expect(editor.getCursorScreenPosition()).toEqual([0, 4]); 79 | 80 | atom.commands.dispatch(editorView, "snippets:next-tab-stop"); 81 | 82 | expect(editor.getCursorScreenPosition()).toEqual([1, 4]); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /spec/selector-solver-spec.js: -------------------------------------------------------------------------------- 1 | const SelectorSolver = require("../lib/selector-solver"); 2 | 3 | describe("selector-to-tab", function () { 4 | var editor; 5 | var editorView; 6 | 7 | function testSelector(selector, expected, expectedCurPos) { 8 | editor.setText(selector); 9 | editor.moveToEndOfLine(); 10 | 11 | atom.commands.dispatch(editorView, "selector-to-tag:solve-selector"); 12 | 13 | expect(editor.getText()).toBe(expected); 14 | 15 | if (expectedCurPos) { 16 | expect(editor.getCursorScreenPosition()).toEqual(expectedCurPos); 17 | } 18 | } 19 | 20 | beforeEach(function () { 21 | waitsForPromise(function () { 22 | return atom.workspace.open("test.html"); 23 | }); 24 | 25 | waitsForPromise(function () { 26 | const selectorToTag = atom.packages.activatePackage("selector-to-tag"); 27 | const snippets = atom.packages.deactivatePackage("snippets"); 28 | return Promise.all([selectorToTag, snippets]); 29 | }); 30 | 31 | runs(function () { 32 | editor = atom.workspace.getActiveTextEditor(); 33 | editor.setText(""); 34 | editorView = atom.views.getView(editor); 35 | }); 36 | }); 37 | 38 | describe("HTML detector", function () { 39 | var solver; 40 | 41 | beforeEach(function () { 42 | solver = new SelectorSolver(); 43 | }); 44 | 45 | it("should handle files with no extension", function () { 46 | expect(solver.isHTML("untitled")).toBe(false); 47 | }); 48 | 49 | it("should detect HTML files", function () { 50 | expect(solver.isHTML("test.HTML")).toBe(true); 51 | }); 52 | }); 53 | 54 | describe("Tag solver", function () { 55 | beforeEach(function () { 56 | atom.config.unset("selector-to-tag.closeSelfclosingTags"); 57 | atom.config.unset("selector-to-tag.expandBlockTags"); 58 | }); 59 | 60 | it("should solve block tags", function () { 61 | testSelector("div", "
", [0, 5]); 62 | }); 63 | 64 | it("should solve inline tags", function () { 65 | testSelector("link", "", [0, 5]); 66 | }); 67 | 68 | it("should solve unknown tags", function () { 69 | testSelector("ceva", "", [0, 6]); 70 | }); 71 | 72 | it("should solve tag with id", function () { 73 | testSelector("div#mama", '
', [0, 15]); 74 | }); 75 | 76 | it("should solve tag with id and class", function () { 77 | testSelector("div#mama.tata", '
', [ 78 | 0, 79 | 28, 80 | ]); 81 | }); 82 | 83 | it("should solve tag with id and multiple classes", function () { 84 | testSelector( 85 | "div#mama.tata.sora", 86 | '
', 87 | [0, 33] 88 | ); 89 | }); 90 | 91 | it("should solve selectors containing - and _", function () { 92 | testSelector( 93 | "some-tag_1#id-1_2.class_1.class-2", 94 | '', 95 | [0, 48] 96 | ); 97 | }); 98 | 99 | it("should solve self-closing tags", function () { 100 | atom.config.set("selector-to-tag.closeSelfclosingTags", true); 101 | testSelector("link", "", [0, 5]); 102 | }); 103 | 104 | it("should expand block tags to multiple lines", function () { 105 | atom.config.set("selector-to-tag.expandBlockTags", true); 106 | testSelector("div#mama", '
\n\n
', [1, 0]); 107 | }); 108 | 109 | it("shouldn't expand tag if class or id is not specified", function () { 110 | testSelector("div#", "div#"); 111 | testSelector("div.", "div."); 112 | }); 113 | 114 | it("should solve to div if only class or id specified", function () { 115 | testSelector("#mama", '
', [0, 15]); 116 | testSelector(".tata", '
', [0, 18]); 117 | }); 118 | 119 | it("should not solve to div if nothing is specified", function () { 120 | testSelector("", ""); 121 | }); 122 | 123 | it("should not create tags from invalid strings", function () { 124 | testSelector("42", "42"); 125 | }); 126 | 127 | it("should remember tag's case", function () { 128 | testSelector("View", ""); 129 | }); 130 | 131 | it("should not solve tags if inside another tag", function () { 132 | editor.setText(''); 133 | editor.setCursorScreenPosition([0, 19]); 134 | atom.commands.dispatch(editorView, "selector-to-tag:solve-selector"); 135 | 136 | expect(editor.getText()).toBe( 137 | '' 138 | ); 139 | }); 140 | 141 | it("should solve tags with a namespace prefix", function () { 142 | testSelector("ns:View", ""); 143 | }); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /lib/selector-solver.js: -------------------------------------------------------------------------------- 1 | require("./utils"); 2 | 3 | function removeFirstChar(string) { 4 | return string.substring(1); 5 | } 6 | 7 | class SelectorSolver { 8 | constructor(availableAPI) { 9 | this.defaultFileTypes = [ 10 | "htm", 11 | "html", 12 | "kit", 13 | "shtml", 14 | "tmpl", 15 | "tpl", 16 | "xhtml", 17 | ]; 18 | this.availableAPI = availableAPI; 19 | 20 | this.selfClosingTags = [ 21 | "area", 22 | "br", 23 | "col", 24 | "embed", 25 | "hr", 26 | "img", 27 | "input", 28 | "link", 29 | "meta", 30 | "param", 31 | ]; 32 | 33 | atom.commands.add("atom-text-editor", { 34 | "selector-to-tag:solve-selector": this.handleCommand.bind(this), 35 | }); 36 | } 37 | 38 | isHTML(filename, grammar) { 39 | if (atom.config.get("selector-to-tag.activeForGrammar")) { 40 | // check for grammar 41 | return grammar === ".text.html.basic"; 42 | } else { 43 | var extension = filename.split("."); 44 | if (extension.length > 1) { 45 | extension = extension[extension.length - 1].toLowerCase(); 46 | return ( 47 | ( 48 | atom.config.get("selector-to-tag.activeForFileTypes") || 49 | this.defaultFileTypes 50 | ).indexOf(extension) !== -1 51 | ); 52 | } 53 | } 54 | return false; 55 | } 56 | 57 | solveSelector( 58 | selector, 59 | solveTagsOnly, 60 | closeSelfclosingTags, 61 | expandBlockTags, 62 | blockTags, 63 | editor 64 | ) { 65 | if (typeof blockTags === "undefined") { 66 | blockTags = []; 67 | } 68 | 69 | // it shouldn't expand tag if class or id is not specified 70 | if (selector.substr(-1) === "#" || selector.substr(-1) === ".") { 71 | return null; 72 | } 73 | 74 | var tag = (/^[a-zA-Z][\w-:]*/.exec(selector) || [])[0]; 75 | const id = (/#[\w-]+/.exec(selector) || []).map(removeFirstChar)[0]; 76 | const classes = /(\.[\w-]+)/g.getAllMatches(selector).map(removeFirstChar); 77 | 78 | if ((solveTagsOnly && tag) || classes.length > 0 || id) { 79 | let string, snippet, isSelfClosing, isBlock; 80 | 81 | if (!tag) { 82 | tag = "div"; 83 | } 84 | 85 | isSelfClosing = this.selfClosingTags.indexOf(tag) !== -1; 86 | isBlock = blockTags.indexOf(tag) !== -1; 87 | 88 | var element = document.createElementNS( 89 | "http://www.w3.org/1999/xhtml", 90 | tag 91 | ); 92 | 93 | if (id) { 94 | element.id = id; 95 | } 96 | 97 | if (classes.length > 0) { 98 | element.className = classes.join(" "); 99 | } 100 | 101 | string = element.outerHTML; 102 | snippet = string; 103 | 104 | if (isSelfClosing) { 105 | if (closeSelfclosingTags) { 106 | string = string.insertAt(string.length - 1, "/"); 107 | } 108 | snippet = snippet.insertAt(snippet.length - 1, "$1"); 109 | } 110 | 111 | if (isBlock) { 112 | if (expandBlockTags) { 113 | string = string.insertAt(string.indexOf(""), "$1"); 123 | } 124 | 125 | return { element, string, isSelfClosing, isBlock, snippet }; 126 | } 127 | 128 | return null; 129 | } 130 | 131 | insertSolvedText(solvedElement, selectorRange, editor) { 132 | if (this.availableAPI.snippetsAPI) { 133 | const snippetsAPI = this.availableAPI.snippetsAPI; 134 | editor.setTextInBufferRange(selectorRange, ""); 135 | snippetsAPI.insertSnippet(solvedElement.snippet, editor); 136 | } else { 137 | editor.setTextInBufferRange(selectorRange, solvedElement.string); 138 | } 139 | } 140 | 141 | handleCommand(event) { 142 | const editor = event.currentTarget.getModel(); 143 | 144 | if (!this.isHTML(editor.getTitle(), editor.getRootScopeDescriptor())) { 145 | event.abortKeyBinding(); 146 | return; 147 | } 148 | 149 | const cursorPosition = editor.getCursorBufferPosition(); 150 | const textBeforeCursor = editor.getTextInBufferRange([ 151 | [0, 0], 152 | cursorPosition, 153 | ]); 154 | 155 | const lastClosingBracketIndex = textBeforeCursor.lastIndexOf(">"); 156 | const lastOpeningBracketIndex = textBeforeCursor.lastIndexOf("<"); 157 | const isInsideTag = lastOpeningBracketIndex > lastClosingBracketIndex; 158 | 159 | if (isInsideTag) { 160 | event.abortKeyBinding(); 161 | return; 162 | } 163 | 164 | const match = /\s*([\w.#-_:]*)$/.exec(textBeforeCursor); 165 | 166 | if (!match || match.length < 1) { 167 | event.abortKeyBinding(); 168 | return; 169 | } 170 | 171 | const expandBlockTags = atom.config.get("selector-to-tag.expandBlockTags"); 172 | const solveTagsOnly = atom.config.get("selector-to-tag.solveTagsOnly"); 173 | const closeSelfclosingTags = atom.config.get( 174 | "selector-to-tag.closeSelfclosingTags" 175 | ); 176 | const blockTags = atom.config.get("selector-to-tag.blockTags"); 177 | 178 | const selector = match[1]; 179 | const solvedElement = this.solveSelector( 180 | selector, 181 | solveTagsOnly, 182 | closeSelfclosingTags, 183 | expandBlockTags, 184 | blockTags, 185 | editor 186 | ); 187 | 188 | if (!solvedElement) { 189 | event.abortKeyBinding(); 190 | return; 191 | } 192 | 193 | const selectorRange = [ 194 | [cursorPosition.row, cursorPosition.column - selector.length], 195 | cursorPosition, 196 | ]; 197 | 198 | if (expandBlockTags && solvedElement.isBlock) { 199 | editor.transact(() => { 200 | this.insertSolvedText(solvedElement, selectorRange, editor); 201 | 202 | if (!this.availableAPI.snippetsAPI) { 203 | editor.selectUp(); 204 | editor.getLastSelection().autoIndentSelectedRows(); 205 | editor.clearSelections(); 206 | editor.moveToEndOfLine(); 207 | } 208 | }); 209 | } else { 210 | this.insertSolvedText(solvedElement, selectorRange, editor); 211 | 212 | if (!this.availableAPI.snippetsAPI) { 213 | const newCursorPosition = editor.getCursorScreenPosition(); 214 | 215 | if (solvedElement.isSelfClosing) { 216 | if (atom.config.get("selector-to-tag.closeSelfclosingTags")) { 217 | newCursorPosition.column -= 2; 218 | } else { 219 | newCursorPosition.column -= 1; 220 | } 221 | 222 | editor.setCursorScreenPosition(newCursorPosition); 223 | } else { 224 | newCursorPosition.column -= solvedElement.element.tagName.length + 3; 225 | editor.setCursorScreenPosition(newCursorPosition); 226 | } 227 | } 228 | } 229 | } 230 | } 231 | 232 | module.exports = SelectorSolver; 233 | --------------------------------------------------------------------------------