├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .stylelintrc ├── README.md ├── gulpfile.js ├── img ├── default-en.png ├── default-ru.png ├── errors-en.png ├── errors-ru.png ├── highlight-en.png └── highlight-ru.png ├── index.html ├── package-lock.json ├── package.json ├── public ├── assets │ ├── css │ │ └── styles.css │ ├── img │ │ ├── bem-error-highlight.png │ │ ├── screen.png │ │ └── user-highlight.png │ └── js │ │ └── script.js ├── en │ └── index.html └── index.html └── src ├── index-src.html ├── js └── script.js ├── scss ├── base.scss ├── blocks │ ├── code-io.scss │ ├── column.scss │ ├── deep-levels.scss │ ├── headers-list.scss │ ├── headers-tree.scss │ ├── highlight.scss │ ├── langs.scss │ ├── messages.scss │ └── tree.scss ├── layout.scss └── styles.scss └── translate ├── en.json └── ru.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true 6 | }, 7 | extends: [`standard`], 8 | globals: { 9 | Atomics: `readonly`, 10 | SharedArrayBuffer: `readonly` 11 | }, 12 | parser: `babel-eslint`, 13 | parserOptions: { 14 | ecmaVersion: 2018, 15 | sourceType: `module` 16 | }, 17 | rules: { 18 | semi: [`error`, `always`], 19 | // fix: Cannot read property `range` of null 20 | //https://github.com/babel/babel-eslint/issues/681#issuecomment-420663038 21 | 'template-curly-spacing' : `off`, 22 | indent: `off`, 23 | quotes: [2, `backtick`] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .publish 3 | .sass-cache 4 | build 5 | node_modules -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": { 4 | "at-rule-no-unknown": [ 5 | true, 6 | ignoreAtRules: [ 7 | "mixin", "include" 8 | ] 9 | ], 10 | "indentation": 2, 11 | "string-quotes": "double", 12 | "no-duplicate-selectors": true, 13 | "color-hex-case": "upper", 14 | "color-hex-length": "short", 15 | "selector-max-id": 0, 16 | "selector-combinator-space-after": "always", 17 | "selector-attribute-quotes": "always", 18 | "selector-type-case": "upper", 19 | "declaration-block-trailing-semicolon": "always", 20 | "declaration-no-important": true, 21 | "declaration-colon-space-before": "never", 22 | "declaration-colon-space-after": "always-single-line", 23 | "declaration-colon-newline-after": null, 24 | "property-no-vendor-prefix": true, 25 | "value-no-vendor-prefix": true, 26 | "number-leading-zero": "never", 27 | "function-url-quotes": "always", 28 | "font-family-name-quotes": "always-where-required", 29 | "comment-whitespace-inside": "always", 30 | "comment-empty-line-before": null, 31 | "custom-property-empty-line-before": null, 32 | "at-rule-no-vendor-prefix": true, 33 | "selector-pseudo-element-colon-notation": "double", 34 | "selector-pseudo-class-parentheses-space-inside": "never", 35 | "selector-no-vendor-prefix": true, 36 | "media-feature-range-operator-space-before": "always", 37 | "media-feature-range-operator-space-after": "always", 38 | "media-feature-parentheses-space-inside": "always", 39 | "media-feature-colon-space-before": "never", 40 | "media-feature-colon-space-after": "always" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # html-tree 2 | 3 | http://yoksel.github.io/html-tree/ 4 | 5 | --- 6 | 7 | **RU** | [EN ↓](#en) 8 | 9 | ## Генератор дерева на основе HTML-разметки. 10 | 11 | Вставьте в левое поле HTML-код, и справа появится дерево документа в виде вложенных списков: 12 | 13 | ![Генератор дерева на основе HTML-разметки](/img/default-ru.png) 14 | 15 | Бонусом делается простая проверка БЭМ-нотации и иерархии заголовков: 16 | 17 | ![Подсветка БЭМ-ошибок](/img/errors-ru.png) 18 | 19 | Кликните по классу, чтобы подсветить его: 20 | 21 | Выборочная подсветка кода 22 | 23 | --- 24 | 25 | [RU ↑](#ru) | **EN** 26 | 27 | ## HTML Tree Generator 28 | 29 | Insert the HTML code to the left textarea, and the document tree will appear on the right as nested lists: 30 | 31 | ![HTML Tree Generator](/img/default-en.png) 32 | 33 | As a bonus there is a simple check of BEM notation and header hierarchy: 34 | 35 | ![Highlighting errors](/img/errors-en.png) 36 | 37 | Click on class to highlight it: 38 | 39 | Highlighting classes 40 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require(`gulp`); 2 | const sass = require(`gulp-sass`)(require('node-sass')); 3 | const sync = require(`browser-sync`).create(); 4 | const reload = sync.reload; 5 | const colors = require(`colors/safe`); 6 | const del = require(`del`); 7 | const mustache = require(`gulp-mustache`); 8 | const rename = require(`gulp-rename`); 9 | 10 | const SERVER_ROOT = `build/`; 11 | 12 | const translates = [ 13 | { 14 | dest: SERVER_ROOT, 15 | url: `./src/translate/ru.json` 16 | }, 17 | { 18 | dest: `${SERVER_ROOT}en/`, 19 | url: `./src/translate/en.json` 20 | } 21 | ]; 22 | 23 | // TEMPLATES 24 | const tmplTasks = translates.map(({ dest, url }) => { 25 | return (done) => { 26 | gulp.src(`./src/index-src.html`) 27 | .pipe(mustache(url)) 28 | .pipe(rename(`index.html`)) 29 | .pipe(gulp.dest(dest)) 30 | .pipe(reload({ stream: true })); 31 | done(); 32 | }; 33 | }); 34 | 35 | gulp.task(`tmpl`, gulp.series(...tmplTasks)); 36 | 37 | // SCSS 38 | gulp.task(`scss`, function () { 39 | return gulp.src(`src/scss/**/styles.scss`) 40 | .pipe(sass().on(`error`, sass.logError)) 41 | .pipe(gulp.dest(`${SERVER_ROOT}assets/css`)) 42 | .pipe(reload({ stream: true })); 43 | }); 44 | 45 | // JS 46 | gulp.task(`js`, function () { 47 | return gulp.src(`src/js/**/*.js`) 48 | .pipe(gulp.dest(`${SERVER_ROOT}assets/js`)) 49 | .pipe(reload({ stream: true })); 50 | }); 51 | 52 | // WATCH FILES 53 | function watchTasks () { 54 | sync.init({ 55 | ui: false, 56 | notify: false, 57 | server: { 58 | baseDir: SERVER_ROOT 59 | } 60 | }); 61 | 62 | gulp.watch([`src/scss/**/*.scss`], gulp.series(`scss`)); 63 | gulp.watch([`src/index-src.html`, `src/translate/**/*`], gulp.series(`tmpl`)); 64 | gulp.watch([`src/js/**/*`], gulp.series(`js`)); 65 | } 66 | 67 | gulp.task(`serve`, gulp.series([`scss`, `js`, `tmpl`], watchTasks)); 68 | 69 | // CLEAN BUILD 70 | gulp.task(`clean`, function (done) { 71 | del([`${SERVER_ROOT}*`]).then(paths => { 72 | console.log(`⬤ Deleted files and folders:\n`, paths.join(`\n`)); 73 | }); 74 | 75 | done(); 76 | }); 77 | 78 | // CLEAN BUILD & COPY FILES TO IT 79 | gulp.task(`build`, gulp.series([`clean`], [`scss`, `js`, `tmpl`])); 80 | 81 | gulp.task(`default`, function () { 82 | console.log(colors.rainbow(`⬤ ================================ ⬤\n`)); 83 | console.log(` AVAILABLE COMMANDS:`); 84 | console.log(` ` + colors.cyan(`-------------------\n`)); 85 | console.log(` ` + colors.yellow(`npm start`) + 86 | ` — run local server with watcher`); 87 | console.log(` ` + colors.green(`npm run build`) + 88 | ` — make build of the project`); 89 | console.log(` ` + colors.cyan(`npm run deploy`) + 90 | ` — make build and publish project to Github Pages`); 91 | console.log(colors.rainbow(`\n⬤ ================================ ⬤`)); 92 | }); 93 | -------------------------------------------------------------------------------- /img/default-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoksel/html-tree/749c3aff3628c8df09e6a572642ab8420813423d/img/default-en.png -------------------------------------------------------------------------------- /img/default-ru.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoksel/html-tree/749c3aff3628c8df09e6a572642ab8420813423d/img/default-ru.png -------------------------------------------------------------------------------- /img/errors-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoksel/html-tree/749c3aff3628c8df09e6a572642ab8420813423d/img/errors-en.png -------------------------------------------------------------------------------- /img/errors-ru.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoksel/html-tree/749c3aff3628c8df09e6a572642ab8420813423d/img/errors-ru.png -------------------------------------------------------------------------------- /img/highlight-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoksel/html-tree/749c3aff3628c8df09e6a572642ab8420813423d/img/highlight-en.png -------------------------------------------------------------------------------- /img/highlight-ru.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoksel/html-tree/749c3aff3628c8df09e6a572642ab8420813423d/img/highlight-ru.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Генератор HTML-дерева 6 | 7 | 8 | 9 |
10 |
11 |

Генератор HTML-дерева

12 |
13 | 14 |
15 |
16 |

Введите код:

17 | 18 | 19 |
20 |

21 | Уровни: 22 | 1 23 |

24 | 25 |
26 | 27 |
28 |
29 |
30 | Похоже, в классах используется БЭМ-нотация, но что-то пошло не так. 31 |
32 |
33 |
34 |
35 |
36 | 37 |
38 |

Структура заголовков:

39 |
40 |
41 |
42 |
43 |
44 |

Структура разметки:

45 |
46 |
47 |
48 |

Чтобы увидеть структуру разметки, вставьте HTML-код в поле слева.

Можно вставлять как отдельные блоки, так и код страницы целиком.

49 |
50 |
51 |
52 |
53 | 54 | 58 |
59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-tree", 3 | "version": "1.0.0", 4 | "description": "Html Tree generator", 5 | "main": "index.html", 6 | "author": "yoksel", 7 | "license": "ISC", 8 | "devDependencies": { 9 | "babel-eslint": "^10.1.0", 10 | "browser-sync": "^2.26.10", 11 | "colors": "^1.1.2", 12 | "del": "^2.2.0", 13 | "eslint": "^6.8.0", 14 | "eslint-config-standard": "^14.1.0", 15 | "eslint-plugin-import": "^2.22.0", 16 | "eslint-plugin-node": "^11.1.0", 17 | "eslint-plugin-promise": "^4.2.1", 18 | "eslint-plugin-standard": "^4.0.1", 19 | "gh-pages": "^3.2.3", 20 | "gulp": "^4.0.2", 21 | "gulp-mustache": "^5.0.0", 22 | "gulp-rename": "2.0.0", 23 | "gulp-ruby-sass": "^2.0.6", 24 | "gulp-sass": "^5.0.0", 25 | "lodash.template": ">=4.5.0", 26 | "node-sass": "^6.0.1", 27 | "stylelint": "^13.0.0", 28 | "stylelint-config-standard": "^19.0.0" 29 | }, 30 | "scripts": { 31 | "start": "gulp serve", 32 | "build": "gulp build", 33 | "deploy": "npm run build && gh-pages -d build -b gh-pages", 34 | "lint": "npm run js:lint && npm run css:lint", 35 | "lint:fix": "npm run js:fix && npm run css:fix", 36 | "js:lint": "eslint src", 37 | "js:fix": "eslint src --fix", 38 | "css:lint": "stylelint **/*.scss", 39 | "css:fix": "stylelint **/*.scss --fix" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /public/assets/css/styles.css: -------------------------------------------------------------------------------- 1 | .gnr-html { 2 | font-size: 16px; } 3 | 4 | .gnr-body { 5 | min-width: 800px; 6 | margin: 0; 7 | background: #EEE; 8 | color: #000; 9 | font: 14px/1.5 Trebuchet MS, sans-serif; } 10 | 11 | H1, 12 | H2, 13 | H3 { 14 | margin-top: 0; 15 | margin-bottom: .8em; 16 | font-weight: normal; } 17 | 18 | A { 19 | color: royalblue; } 20 | A:hover { 21 | text-decoration: none; } 22 | 23 | UL, 24 | LI { 25 | padding: 0; 26 | margin: 0; 27 | list-style: none; } 28 | 29 | .gnr-hidden { 30 | display: none; } 31 | 32 | .gnr-page { 33 | margin: 2rem auto; 34 | padding: 2rem 3rem 3rem; 35 | width: 80%; 36 | min-width: 800px; 37 | max-width: 1200px; 38 | box-sizing: content-box; 39 | background: #FFF; 40 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); } 41 | 42 | .gnr-page__header { 43 | position: relative; 44 | margin-bottom: 2.65rem; 45 | padding: 1rem 0; 46 | border-bottom: 3px solid #EEE; } 47 | 48 | .gnr-page__title { 49 | margin-bottom: 0; 50 | text-align: center; 51 | font-size: 2em; } 52 | 53 | .gnr-page__container { 54 | display: flex; 55 | justify-content: space-between; } 56 | 57 | .gnr-page__footer { 58 | margin-top: 3rem; 59 | padding-top: 1rem; 60 | border-top: 3px solid #EEE; 61 | display: flex; 62 | justify-content: space-between; } 63 | 64 | .gnr-page__langs { 65 | position: absolute; 66 | top: 0; 67 | right: 0; 68 | display: flex; } 69 | 70 | .gnr-page__langs-item + .gnr-page__langs-item { 71 | margin-left: .5rem; } 72 | 73 | .gnr-column { 74 | width: 48%; } 75 | 76 | .gnr-column__title { 77 | font-size: 1.5em; } 78 | 79 | .gnr-tree { 80 | min-height: 200px; 81 | padding: 1rem; 82 | box-sizing: border-box; 83 | border: 1px solid #DDD; 84 | display: flex; 85 | justify-content: center; 86 | align-items: center; } 87 | 88 | .gnr-tree__placeholder { 89 | width: 80%; 90 | text-align: center; 91 | font-size: 14px; 92 | color: #999; } 93 | 94 | .gnr-tree__content { 95 | max-width: 100%; } 96 | 97 | /* Level 98 | ------------------------------ */ 99 | .gnr-level { 100 | position: relative; 101 | margin-bottom: .5rem; 102 | margin-left: 1.2rem; 103 | background: linear-gradient(to right, lawngreen 1px, transparent 2px); 104 | background-repeat: no-repeat; 105 | background-position: 10px 0; } 106 | .gnr-level::before { 107 | content: ""; 108 | display: block; 109 | position: absolute; 110 | z-index: 0; 111 | left: -1px; 112 | bottom: 0; 113 | width: 2rem; 114 | margin-left: 10px; 115 | height: 10px; 116 | background: white; 117 | border-top: 1px solid lawngreen; } 118 | .gnr-level--0::before { 119 | height: 10px; } 120 | 121 | .gnr-level__item { 122 | position: relative; } 123 | .gnr-level__item::before { 124 | content: ""; 125 | display: block; 126 | position: absolute; 127 | width: .7rem; 128 | height: .08rem; 129 | margin-left: -.55rem; 130 | margin-top: .6rem; 131 | background: lawngreen; } 132 | .gnr-level--0 > .gnr-level__item::before { 133 | content: none; } 134 | 135 | .gnr-elem { 136 | padding-left: .3rem; 137 | background: white; } 138 | 139 | .gnr-elem__tag { 140 | background: #FFF; 141 | color: royalblue; } 142 | 143 | .gnr-class__dot { 144 | position: relative; 145 | padding: 0 .05em; 146 | font-size: 1.2em; 147 | line-height: 1; } 148 | 149 | .gnr-class__item { 150 | white-space: nowrap; 151 | cursor: pointer; } 152 | 153 | .gnr-code-input { 154 | width: 100%; 155 | height: 200px; 156 | margin-bottom: 2rem; 157 | padding: 1rem; 158 | box-sizing: border-box; 159 | border: 1px solid #CCC; 160 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.2) inset; 161 | font-size: inherit; } 162 | 163 | .gnr-code-output { 164 | display: none; } 165 | 166 | .gnr-deep__title { 167 | font-size: 1.17em; } 168 | 169 | .gnr-deep__digit { 170 | display: inline-block; 171 | font-weight: bold; 172 | color: crimson; } 173 | 174 | .gnr-deep__range { 175 | width: 100%; 176 | margin: 0; 177 | cursor: pointer; } 178 | 179 | .gnr-messages { 180 | margin-top: 2rem; } 181 | 182 | .gnr-message { 183 | margin-top: .75rem; } 184 | .gnr-message:first-child { 185 | margin-top: 0; } 186 | 187 | .gnr-message__content { 188 | padding: .75rem; 189 | background: lemonchiffon; } 190 | 191 | .headers__list { 192 | margin: 0; 193 | display: flex; 194 | flex-wrap: wrap; } 195 | 196 | .headers__item { 197 | margin-left: .5rem; } 198 | .headers__item--notfound { 199 | opacity: .3; } 200 | 201 | .gnr-message--headers-tree { 202 | margin-top: 2rem; } 203 | 204 | .gnr-message__content--headers-tree { 205 | background: #d7ebad; 206 | font-size: 10px; 207 | /* stylelint-disable no-descending-specificity */ 208 | /* stylelint-enable no-descending-specificity */ } 209 | .gnr-message__content--headers-tree H1, 210 | .gnr-message__content--headers-tree H2, 211 | .gnr-message__content--headers-tree H3, 212 | .gnr-message__content--headers-tree H4, 213 | .gnr-message__content--headers-tree H5, 214 | .gnr-message__content--headers-tree H6 { 215 | margin-top: 0; 216 | margin-bottom: .85rem; 217 | line-height: 1; 218 | font-weight: bold; } 219 | .gnr-message__content--headers-tree H1:last-child, 220 | .gnr-message__content--headers-tree H2:last-child, 221 | .gnr-message__content--headers-tree H3:last-child, 222 | .gnr-message__content--headers-tree H4:last-child, 223 | .gnr-message__content--headers-tree H5:last-child, 224 | .gnr-message__content--headers-tree H6:last-child { 225 | margin-bottom: 0; } 226 | .gnr-message__content--headers-tree H1 SPAN, 227 | .gnr-message__content--headers-tree H2 SPAN, 228 | .gnr-message__content--headers-tree H3 SPAN, 229 | .gnr-message__content--headers-tree H4 SPAN, 230 | .gnr-message__content--headers-tree H5 SPAN, 231 | .gnr-message__content--headers-tree H6 SPAN { 232 | font-size: .75em; 233 | opacity: .5; } 234 | .gnr-message__content--headers-tree H1 { 235 | font-size: 2em; } 236 | .gnr-message__content--headers-tree H2 { 237 | margin-left: 1rem; 238 | font-size: 1.75em; } 239 | .gnr-message__content--headers-tree H3 { 240 | margin-left: 3rem; 241 | font-size: 1.5em; } 242 | .gnr-message__content--headers-tree H4 { 243 | margin-left: 4rem; 244 | font-size: 1.25em; } 245 | .gnr-message__content--headers-tree H5 { 246 | margin-left: 5rem; 247 | font-size: 1em; } 248 | .gnr-message__content--headers-tree H6 { 249 | margin-left: 6rem; 250 | font-size: .75em; } 251 | 252 | .gnr-highlight-bem { 253 | box-shadow: -0.2em 0 0 0 gold, 0.2em 0 0 0 gold; 254 | background: gold; } 255 | 256 | [data-color="fuchsia"] { 257 | box-shadow: -0.2em 0 0 0 fuchsia, 0.2em 0 0 0 fuchsia; 258 | background: fuchsia; } 259 | 260 | [data-color="yellow"] { 261 | box-shadow: -0.2em 0 0 0 yellow, 0.2em 0 0 0 yellow; 262 | background: yellow; } 263 | 264 | [data-color="lime"] { 265 | box-shadow: -0.2em 0 0 0 lime, 0.2em 0 0 0 lime; 266 | background: lime; } 267 | 268 | [data-color="aqua"] { 269 | box-shadow: -0.2em 0 0 0 aqua, 0.2em 0 0 0 aqua; 270 | background: aqua; } 271 | -------------------------------------------------------------------------------- /public/assets/img/bem-error-highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoksel/html-tree/749c3aff3628c8df09e6a572642ab8420813423d/public/assets/img/bem-error-highlight.png -------------------------------------------------------------------------------- /public/assets/img/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoksel/html-tree/749c3aff3628c8df09e6a572642ab8420813423d/public/assets/img/screen.png -------------------------------------------------------------------------------- /public/assets/img/user-highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoksel/html-tree/749c3aff3628c8df09e6a572642ab8420813423d/public/assets/img/user-highlight.png -------------------------------------------------------------------------------- /public/assets/js/script.js: -------------------------------------------------------------------------------- 1 | var doc = document; 2 | var codeInput = doc.querySelector(`.gnr-code-input`); 3 | var treeContent = doc.querySelector(`.gnr-tree__content`); 4 | var treePlaceHolder = doc.querySelector(`.gnr-tree__placeholder`); 5 | var rangeDeep = doc.querySelector(`.gnr-deep__range`); 6 | var valDeep = doc.querySelector(`.gnr-deep__digit`); 7 | var maxDeep = 1; 8 | var bemMessage = doc.querySelector(`.gnr-message--bem`); 9 | var headersMessage = doc.querySelector(`.gnr-message--headers`); 10 | var headersMessageContent = doc.querySelector(`.gnr-message--headers .gnr-message__content`); 11 | var headersMessageTree = doc.querySelector(`.gnr-message--headers-tree`); 12 | var headersMessageTreeContent = doc.querySelector(`.gnr-message--headers-tree .gnr-message__content`); 13 | var headersLevels = {}; 14 | var headersOrder = [`H1`, `H2`, `H3`, `H4`, `H5`, `H6`]; 15 | var headersList = []; 16 | var isWHolePage = false; 17 | var hasBemWarning = false; 18 | var bodyClass = ``; 19 | 20 | var wholePageMarkers = [`META`, `TITLE`, `LINK`]; 21 | var skippedTags = [`SCRIPT`, `META`, `TITLE`, `LINK`, `NOSCRIPT`, `BR`]; 22 | 23 | var highlightColorNum = 0; 24 | 25 | var styleElem = doc.createElement(`style`); 26 | doc.head.appendChild(styleElem); 27 | 28 | // DEV ONLY 29 | // Add to textarea code for easy testing 30 | runDev(); 31 | 32 | // ------------------------------ 33 | 34 | codeInput.oninput = function () { 35 | setHeadersDefs(); 36 | isWHolePage = false; 37 | hasBemWarning = false; 38 | maxDeep = 1; 39 | 40 | headersMessage.classList.add(`gnr-hidden`); 41 | headersMessageTree.classList.add(`gnr-hidden`); 42 | bemMessage.classList.add(`gnr-hidden`); 43 | 44 | createTreeFromHTML(this.value); 45 | 46 | addClassesActions(); 47 | }; 48 | 49 | // ------------------------------ 50 | 51 | function setHeadersDefs () { 52 | headersLevels = { 53 | H1: false, 54 | H2: false, 55 | H3: false, 56 | H4: false, 57 | H5: false, 58 | H6: false 59 | }; 60 | 61 | headersList = []; 62 | } 63 | 64 | // ------------------------------ 65 | 66 | function createTreeFromHTML (code) { 67 | var codeOutput = document.createElement(`div`); 68 | 69 | if (!code) { 70 | treeContent.classList.add(`gnr-hidden`); 71 | treePlaceHolder.classList.remove(`gnr-hidden`); 72 | setRange(); 73 | return; 74 | } 75 | 76 | // Fix for minified code 77 | code = code.replace(/>\n<`); 78 | 79 | bodyClass = getBodyClass(code); 80 | 81 | if (bodyClass) { 82 | bodyClass.forEach(function (item) { 83 | item && codeOutput.classList.add(item); 84 | }); 85 | } 86 | codeOutput.innerHTML = code; 87 | 88 | var items = makeList(codeOutput, 1); 89 | 90 | if (treeContent.childElementCount > 0) { 91 | treeContent.removeChild(treeContent.firstElementChild); 92 | } 93 | 94 | var list = doc.createElement(`ul`); 95 | list.classList.add(`gnr-level`, `gnr-level--0`); 96 | list.appendChild(items); 97 | treeContent.appendChild(list); 98 | 99 | treeContent.classList.remove(`gnr-hidden`); 100 | treePlaceHolder.classList.add(`gnr-hidden`); 101 | 102 | setRange(); 103 | 104 | showCodeErrors(); 105 | } 106 | 107 | // ------------------------------ 108 | 109 | function makeList (elem, level) { 110 | var item = doc.createElement(`li`); 111 | item.classList.add(`gnr-level__item`); 112 | var tagName = elem.tagName; 113 | // elem.className not appropriate for svg 114 | var className = elem.classList.value; 115 | elem.classList.forEach = [].forEach; 116 | elem.children.forEach = [].forEach; 117 | 118 | if (!elem.customDataSet) { 119 | elem.customDataSet = { 120 | prefixes: {}, 121 | level: level 122 | }; 123 | } 124 | 125 | if (level === 1) { 126 | tagName = `BODY`; 127 | } 128 | 129 | var liContent = doc.createElement(`div`); 130 | liContent.classList.add(`gnr-level__elem`, `gnr-elem`); 131 | 132 | var tagSpan = doc.createElement(`span`); 133 | tagSpan.classList.add(`gnr-elem__tag`); 134 | tagSpan.innerHTML = tagName; 135 | 136 | // Check headers levels 137 | if (headersLevels[tagName] !== undefined) { 138 | headersLevels[tagName] = true; 139 | headersList.push({ 140 | tagName: tagName, 141 | text: elem.innerText 142 | }); 143 | } 144 | 145 | liContent.appendChild(tagSpan); 146 | 147 | addClassesAsPrefixes(elem); 148 | 149 | if (className) { 150 | checkBemForElem(elem); 151 | 152 | var classSpan = doc.createElement(`span`); 153 | classSpan.classList.add(`gnr-elem__class`, `gnr-class`); 154 | 155 | elem.classList.forEach(function (classItem, i) { 156 | var classItemSpan = doc.createElement(`span`); 157 | classItemSpan.classList.add(`gnr-class__item`); 158 | classItemSpan.innerHTML += classItem; 159 | 160 | // Check valid Bem naiming 161 | if (elem.classList.validBem && 162 | elem.classList.validBem[classItem] === false) { 163 | classItemSpan.classList.add(`gnr-highlight-bem`); 164 | } 165 | 166 | classSpan.appendChild(classItemSpan); 167 | 168 | if (i < elem.classList.length - 1) { 169 | classSpan.innerHTML += ` `; 170 | } 171 | }); 172 | 173 | var classDotSpan = doc.createElement(`span`); 174 | classDotSpan.classList.add(`gnr-class__dot`); 175 | classDotSpan.innerHTML = `.`; 176 | liContent.appendChild(classDotSpan); 177 | 178 | liContent.appendChild(classSpan); 179 | } 180 | 181 | item.appendChild(liContent); 182 | 183 | if (elem.children) { 184 | var childrenList = doc.createElement(`ul`); 185 | childrenList.classList.add(`gnr-level`, `gnr-level--` + level); 186 | 187 | level++; 188 | 189 | elem.children.forEach(function (child) { 190 | checkIsWholePage(child); 191 | 192 | if (!checkIsSkippedTag(child)) { 193 | var newElem = makeList(child, level); 194 | 195 | if (newElem) { 196 | childrenList.appendChild(newElem); 197 | } 198 | } 199 | }); 200 | 201 | if (childrenList.children.length > 0) { 202 | if (level > maxDeep) { 203 | maxDeep = level; 204 | } 205 | 206 | item.appendChild(childrenList); 207 | } 208 | } 209 | 210 | return item; 211 | } 212 | 213 | // ------------------------------ 214 | 215 | function addClassesActions () { 216 | var colors = [`aqua`, `lime`, `yellow`, `fuchsia`]; 217 | 218 | var classItemSpanList = document.querySelectorAll(`.gnr-class__item`); 219 | 220 | classItemSpanList.forEach(function (classItemSpan) { 221 | classItemSpan.onclick = function () { 222 | var color = colors[highlightColorNum]; 223 | 224 | if (this.dataset.color && this.dataset.color !== ``) { 225 | color = ``; 226 | } 227 | 228 | this.dataset.color = color; 229 | }; 230 | }); 231 | } 232 | 233 | // ------------------------------ 234 | 235 | function checkBemForElem (elem) { 236 | // elem.className not appropriate for svg 237 | var className = elem.classList.value; 238 | elem.classList.forEach = [].forEach; 239 | 240 | if (className.indexOf(`__`) < 0 && 241 | className.indexOf(`--`) < 0) { 242 | return; 243 | } 244 | 245 | elem.classList.validBem = {}; 246 | elem.classList.forEach(function (classItem) { 247 | // Check first part of class with __ (block name) 248 | if (classItem.split(`__`).length > 1) { 249 | var prefixCorrect = false; 250 | var prefix = classItem.split(`__`)[0]; 251 | 252 | if (elem.customDataSet.prefixes[prefix]) { 253 | prefixCorrect = true; 254 | } 255 | 256 | elem.classList.validBem[classItem] = prefixCorrect; 257 | 258 | if (!prefixCorrect) { 259 | hasBemWarning = true; 260 | } 261 | } 262 | 263 | // Check first part of class with -- (modificator) 264 | if (classItem.split(`--`).length > 1) { 265 | var modifPrefixCorrect = false; 266 | var modifPrefix = classItem.split(`--`)[0]; 267 | 268 | if (elem.classList.contains(modifPrefix)) { 269 | modifPrefixCorrect = true; 270 | } 271 | 272 | elem.classList.validBem[classItem] = modifPrefixCorrect; 273 | 274 | if (!modifPrefixCorrect) { 275 | hasBemWarning = true; 276 | } 277 | } 278 | }); 279 | } 280 | 281 | // ------------------------------ 282 | 283 | function addClassesAsPrefixes (elem) { 284 | var classList = elem.classList; 285 | classList.forEach = [].forEach; 286 | 287 | copyPrefixes(elem); 288 | 289 | classList.forEach(function (classItem) { 290 | // Copy only block names 291 | if (classItem.split(`__`).length === 1 && 292 | classItem.split(`--`).length === 1) { 293 | elem.customDataSet.prefixes[classItem] = classItem; 294 | } 295 | }); 296 | } 297 | 298 | // ------------------------------ 299 | 300 | function copyPrefixes (elem) { 301 | var parent = elem.parentNode; 302 | 303 | if (!parent) { 304 | return; 305 | } 306 | 307 | for (var prefix in parent.customDataSet.prefixes) { 308 | elem.customDataSet.prefixes[prefix] = prefix; 309 | } 310 | } 311 | 312 | // ------------------------------ 313 | 314 | function setRange () { 315 | rangeDeep.max = maxDeep; 316 | rangeDeep.value = maxDeep; 317 | valDeep.innerHTML = maxDeep; 318 | } 319 | 320 | // ------------------------------ 321 | 322 | rangeDeep.oninput = function () { 323 | var level = +this.value; 324 | var styles = `.gnr-level--` + level + `{ display: none }`; 325 | styleElem.innerHTML = styles; 326 | valDeep.innerHTML = this.value; 327 | }; 328 | 329 | // ------------------------------ 330 | 331 | function showCodeErrors () { 332 | showBemMessage(); 333 | checkHeadersLevels(); 334 | printHeadersTree(); 335 | } 336 | 337 | // ------------------------------ 338 | 339 | function showBemMessage () { 340 | bemMessage.classList.toggle(`gnr-hidden`, !hasBemWarning); 341 | } 342 | 343 | // ------------------------------ 344 | 345 | function checkHeadersLevels () { 346 | var isWrongOrder = false; 347 | var realOrder = doc.createElement(`dl`); 348 | realOrder.classList.add(`headers__list`); 349 | var maxUsedHeaders = 0; 350 | var tempHeadersStack = 0; 351 | var longestHeadersStack = 0; 352 | 353 | var realOrderDt = doc.createElement(`dt`); 354 | realOrderDt.classList.add(`headers__title`); 355 | realOrderDt.innerHTML = `Заголовки: `; 356 | realOrder.appendChild(realOrderDt); 357 | 358 | for (var key in headersLevels) { 359 | if (headersLevels[key]) { 360 | maxUsedHeaders++; 361 | tempHeadersStack++; 362 | } else { 363 | if (longestHeadersStack < tempHeadersStack) { 364 | longestHeadersStack = tempHeadersStack; 365 | } 366 | tempHeadersStack = 0; 367 | } 368 | } 369 | 370 | if (maxUsedHeaders > longestHeadersStack) { 371 | isWrongOrder = true; 372 | } else if (isWHolePage && !headersLevels.H1) { 373 | isWrongOrder = true; 374 | } 375 | 376 | if (isWrongOrder) { 377 | headersOrder.forEach(function (headerItem) { 378 | var headerItemSpan = doc.createElement(`dd`); 379 | headerItemSpan.classList.add(`headers__item`); 380 | headerItemSpan.innerHTML = headerItem; 381 | 382 | if (headersLevels[headerItem]) { 383 | headerItemSpan.classList.add(`headers__item--found`); 384 | } else { 385 | headerItemSpan.classList.add(`headers__item--notfound`); 386 | } 387 | 388 | realOrder.appendChild(headerItemSpan); 389 | }); 390 | 391 | if (headersMessageContent.firstChild) { 392 | headersMessageContent.removeChild(headersMessageContent.firstChild); 393 | } 394 | headersMessageContent.appendChild(realOrder); 395 | } 396 | 397 | headersMessage.classList.toggle(`gnr-hidden`, !isWrongOrder); 398 | } 399 | 400 | // ------------------------------ 401 | 402 | function printHeadersTree () { 403 | var out = ``; 404 | 405 | if (headersList.length === 0) { 406 | return; 407 | } 408 | 409 | for (var i = 0; i < headersList.length; i++) { 410 | var tag = headersList[i].tagName; 411 | var text = headersList[i].text; 412 | 413 | out += `<` + tag + `>` + tag + ` ` + text + ``; 414 | } 415 | 416 | headersMessageTreeContent.innerHTML = out; 417 | headersMessageTree.classList.remove(`gnr-hidden`); 418 | } 419 | 420 | // ------------------------------ 421 | 422 | function checkIsSkippedTag (elem) { 423 | return skippedTags.indexOf(elem.tagName) >= 0; 424 | } 425 | 426 | // ------------------------------ 427 | 428 | function checkIsWholePage (elem) { 429 | if (wholePageMarkers.indexOf(elem.tagName) >= 0) { 430 | isWHolePage = true; 431 | } 432 | } 433 | 434 | // ------------------------------ 435 | 436 | function getBodyClass (code) { 437 | var result = code.match(/]*class="(.*)"/); 438 | 439 | if (result) { 440 | return result[1].split(` `); 441 | } 442 | 443 | return ``; 444 | } 445 | 446 | // ------------------------------ 447 | 448 | function runDev () { 449 | var testMarkup = `

Title

Title

Content

Title

Content
`; 450 | codeInput.value = testMarkup; 451 | setHeadersDefs(); 452 | hasBemWarning = false; 453 | createTreeFromHTML(testMarkup); 454 | } 455 | -------------------------------------------------------------------------------- /public/en/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HTML-tree generator 6 | 7 | 8 | 9 |
10 |
11 |

HTML-tree generator

12 | 13 |
    14 |
  • 15 | EN 16 |
  • 17 |
  • 18 | RU 19 |
  • 20 |
21 |
22 | 23 |
24 |
25 |

Input html:

26 | 27 | 28 |
29 |

30 | Levels: 31 | 1 32 |

33 | 34 |
35 | 36 |
37 |
38 |
39 | Похоже, в классах используется БЭМ-нотация, но что-то пошло не так. 40 |
41 |
42 |
43 |
44 |
45 | 46 |
47 |

Headers tree:

48 |
49 |
50 |
51 |
52 |
53 |

Markup tree:

54 |
55 |
56 |
57 |

Чтобы увидеть структуру разметки, вставьте HTML-код в поле слева.

Можно вставлять как отдельные блоки, так и код страницы целиком.

58 |
59 |
60 |
61 |
62 | 63 | 67 |
68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Генератор HTML-дерева 6 | 7 | 8 | 9 |
10 |
11 |

Генератор HTML-дерева

12 | 13 |
    14 |
  • 15 | EN 16 |
  • 17 |
  • 18 | RU 19 |
  • 20 |
21 |
22 | 23 |
24 |
25 |

Введите код:

26 | 27 | 28 |
29 |

30 | Уровни: 31 | 1 32 |

33 | 34 |
35 | 36 |
37 |
38 |
39 | Похоже, в классах используется БЭМ-нотация, но что-то пошло не так. 40 |
41 |
42 |
43 |
44 |
45 | 46 |
47 |

Структура заголовков:

48 |
49 |
50 |
51 |
52 |
53 |

Структура разметки:

54 |
55 |
56 |
57 |

Чтобы увидеть структуру разметки, вставьте HTML-код в поле слева.

Можно вставлять как отдельные блоки, так и код страницы целиком.

58 |
59 |
60 |
61 |
62 | 63 | 67 |
68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/index-src.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{title}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

{{title}}

19 | 20 |
    21 | {{#langs}} 22 |
  • 23 | {{{.}}} 24 |
  • 25 | {{/langs}} 26 |
27 |
28 | 29 |
30 |
31 |

{{inputCode}}:

32 | 36 | 37 |
38 |

39 | {{levels}}: 40 | 1 41 |

42 | 43 |
44 | 45 |
46 |
47 |
48 | {{{bemError}}} 49 |
50 |
51 |
52 |
53 |
54 | 55 |
56 |

{{headersTree}}:

57 |
58 |
59 |
60 |
61 |
62 |

{{markupTree}}:

63 |
64 |
65 |
66 | {{{tip}}} 67 |
68 |
69 |
70 |
71 | 72 | 76 |
77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/js/script.js: -------------------------------------------------------------------------------- 1 | const doc = document; 2 | const codeInput = doc.querySelector(`.gnr-code-input`); 3 | const treeContent = doc.querySelector(`.gnr-tree__content`); 4 | const treePlaceHolder = doc.querySelector(`.gnr-tree__placeholder`); 5 | const rangeDeep = doc.querySelector(`.gnr-deep__range`); 6 | const valDeep = doc.querySelector(`.gnr-deep__digit`); 7 | let maxDeep = 1; 8 | const bemMessage = doc.querySelector(`.gnr-message--bem`); 9 | const headersMessage = doc.querySelector(`.gnr-message--headers`); 10 | const headersMessageContent = doc.querySelector(`.gnr-message--headers .gnr-message__content`); 11 | const headersMessageTree = doc.querySelector(`.gnr-message--headers-tree`); 12 | const headersMessageTreeContent = doc.querySelector(`.gnr-message--headers-tree .gnr-message__content`); 13 | let headersLevels = {}; 14 | const headersOrder = [`H1`, `H2`, `H3`, `H4`, `H5`, `H6`]; 15 | let headersList = []; 16 | let isWHolePage = false; 17 | let hasBemWarning = false; 18 | let bodyClass = ``; 19 | let htmlClass = ``; 20 | 21 | const wholePageMarkers = [`META`, `TITLE`, `LINK`]; 22 | const skippedTags = [`SCRIPT`, `META`, `TITLE`, `LINK`, `NOSCRIPT`, `BR`]; 23 | 24 | const highlightColorNum = 0; 25 | 26 | const styleElem = doc.createElement(`style`); 27 | doc.head.appendChild(styleElem); 28 | 29 | // DEV ONLY 30 | // Add to textarea code for easy testing 31 | // runDev(); 32 | 33 | // ------------------------------ 34 | 35 | codeInput.oninput = function () { 36 | setHeadersDefs(); 37 | isWHolePage = false; 38 | hasBemWarning = false; 39 | maxDeep = 1; 40 | 41 | headersMessage.classList.add(`gnr-hidden`); 42 | headersMessageTree.classList.add(`gnr-hidden`); 43 | bemMessage.classList.add(`gnr-hidden`); 44 | 45 | createTreeFromHTML(this.value); 46 | 47 | addClassesActions(); 48 | }; 49 | 50 | // ------------------------------ 51 | 52 | function setHeadersDefs () { 53 | headersLevels = { 54 | H1: false, 55 | H2: false, 56 | H3: false, 57 | H4: false, 58 | H5: false, 59 | H6: false 60 | }; 61 | 62 | headersList = []; 63 | } 64 | 65 | // ------------------------------ 66 | 67 | function createTreeFromHTML (code) { 68 | const codeOutput = document.createElement(`div`); 69 | let codeOutputTarget = codeOutput; 70 | 71 | if (!code) { 72 | treeContent.classList.add(`gnr-hidden`); 73 | treePlaceHolder.classList.remove(`gnr-hidden`); 74 | setRange(); 75 | return; 76 | } 77 | 78 | // Fix for minified code 79 | code = code.replace(/>\s*\n<`); 80 | 81 | bodyClass = getTagClass(code); 82 | htmlClass = getTagClass(code, `html`); 83 | 84 | if (htmlClass) { 85 | // Add inner container to show elements in proper order 86 | codeOutputTarget = document.createElement(`div`); 87 | codeOutput.append(codeOutputTarget); 88 | 89 | htmlClass.forEach(function (item) { 90 | // Add classes to outer element 91 | item && codeOutput.classList.add(item); 92 | }); 93 | } 94 | if (bodyClass) { 95 | bodyClass.forEach(function (item) { 96 | item && codeOutputTarget.classList.add(item); 97 | }); 98 | } 99 | 100 | codeOutputTarget.innerHTML = code; 101 | 102 | const items = makeList(codeOutput, 1); 103 | 104 | if (treeContent.childElementCount > 0) { 105 | treeContent.removeChild(treeContent.firstElementChild); 106 | } 107 | 108 | const list = doc.createElement(`ul`); 109 | list.classList.add(`gnr-level`, `gnr-level--0`); 110 | list.appendChild(items); 111 | treeContent.appendChild(list); 112 | 113 | treeContent.classList.remove(`gnr-hidden`); 114 | treePlaceHolder.classList.add(`gnr-hidden`); 115 | 116 | setRange(); 117 | 118 | showCodeErrors(); 119 | } 120 | 121 | // ------------------------------ 122 | 123 | function makeList (elem, level) { 124 | const item = doc.createElement(`li`); 125 | item.classList.add(`gnr-level__item`); 126 | let tagName = elem.tagName; 127 | // elem.className not appropriate for svg 128 | const className = elem.classList.value; 129 | elem.classList.forEach = [].forEach; 130 | elem.children.forEach = [].forEach; 131 | 132 | if (!elem.customDataSet) { 133 | elem.customDataSet = { 134 | prefixes: {}, 135 | level: level 136 | }; 137 | } 138 | 139 | if (htmlClass) { 140 | if (level === 1) { 141 | tagName = `HTML`; 142 | } else if (level === 2) { 143 | tagName = `BODY`; 144 | } 145 | } else if (level === 1) { 146 | tagName = `BODY`; 147 | } 148 | 149 | const liContent = doc.createElement(`div`); 150 | liContent.classList.add(`gnr-level__elem`, `gnr-elem`); 151 | 152 | const tagSpan = doc.createElement(`span`); 153 | tagSpan.classList.add(`gnr-elem__tag`); 154 | tagSpan.innerHTML = tagName; 155 | 156 | // Check headers levels 157 | if (headersLevels[tagName] !== undefined) { 158 | headersLevels[tagName] = true; 159 | headersList.push({ 160 | tagName: tagName, 161 | text: elem.innerText 162 | }); 163 | } 164 | 165 | liContent.appendChild(tagSpan); 166 | 167 | addClassesAsPrefixes(elem); 168 | 169 | if (className) { 170 | checkBemForElem(elem); 171 | 172 | const classSpan = doc.createElement(`span`); 173 | classSpan.classList.add(`gnr-elem__class`, `gnr-class`); 174 | 175 | elem.classList.forEach(function (classItem, i) { 176 | const classItemSpan = doc.createElement(`span`); 177 | classItemSpan.classList.add(`gnr-class__item`); 178 | classItemSpan.innerHTML += classItem; 179 | 180 | // Check valid Bem naiming 181 | if (elem.classList.validBem && 182 | elem.classList.validBem[classItem] === false) { 183 | classItemSpan.classList.add(`gnr-highlight-bem`); 184 | } 185 | 186 | classSpan.appendChild(classItemSpan); 187 | 188 | if (i < elem.classList.length - 1) { 189 | classSpan.innerHTML += ` `; 190 | } 191 | }); 192 | 193 | const classDotSpan = doc.createElement(`span`); 194 | classDotSpan.classList.add(`gnr-class__dot`); 195 | classDotSpan.innerHTML = `.`; 196 | liContent.appendChild(classDotSpan); 197 | 198 | liContent.appendChild(classSpan); 199 | } 200 | 201 | item.appendChild(liContent); 202 | 203 | if (elem.children) { 204 | const childrenList = doc.createElement(`ul`); 205 | childrenList.classList.add(`gnr-level`, `gnr-level--${level}`); 206 | 207 | level++; 208 | 209 | elem.children.forEach(function (child) { 210 | checkIsWholePage(child); 211 | 212 | if (!checkIsSkippedTag(child)) { 213 | const newElem = makeList(child, level); 214 | 215 | if (newElem) { 216 | childrenList.appendChild(newElem); 217 | } 218 | } 219 | }); 220 | 221 | if (childrenList.children.length > 0) { 222 | if (level > maxDeep) { 223 | maxDeep = level; 224 | } 225 | 226 | item.appendChild(childrenList); 227 | } 228 | } 229 | 230 | return item; 231 | } 232 | 233 | // ------------------------------ 234 | 235 | function addClassesActions () { 236 | const colors = [`aqua`, `lime`, `yellow`, `fuchsia`]; 237 | 238 | const classItemSpanList = document.querySelectorAll(`.gnr-class__item`); 239 | 240 | classItemSpanList.forEach(function (classItemSpan) { 241 | classItemSpan.onclick = function () { 242 | let color = colors[highlightColorNum]; 243 | 244 | if (this.dataset.color && this.dataset.color !== ``) { 245 | color = ``; 246 | } 247 | 248 | this.dataset.color = color; 249 | }; 250 | }); 251 | } 252 | 253 | // ------------------------------ 254 | 255 | function checkBemForElem (elem) { 256 | // elem.className not appropriate for svg 257 | const className = elem.classList.value; 258 | const hasDashesDelimiter = className.indexOf(`--`) >= 0; 259 | const hasUnderlinesDelimiter = className.indexOf(`__`) >= 0; 260 | const matchSingleUnderline = className.match(/[^_]_[^_]/); 261 | 262 | if (!hasDashesDelimiter && 263 | !hasUnderlinesDelimiter && 264 | !matchSingleUnderline) { 265 | return; 266 | } 267 | 268 | elem.classList.forEach = [].forEach; 269 | elem.classList.validBem = {}; 270 | 271 | elem.classList.forEach(function (classItem) { 272 | const hasDashesDelimiter = classItem.indexOf(`--`) >= 0; 273 | const hasUnderlinesDelimiter = classItem.indexOf(`__`) >= 0; 274 | const matchSingleUnderline = classItem.match(/[^_]_[^_]/); 275 | 276 | if (!hasUnderlinesDelimiter && 277 | !hasDashesDelimiter && 278 | !matchSingleUnderline) { 279 | return; 280 | } 281 | 282 | // Check first part of class with __ (block name) 283 | if (hasUnderlinesDelimiter) { 284 | let prefixCorrect = false; 285 | const prefix = classItem.split(`__`)[0]; 286 | // Example: wrapper wrapper__container 287 | const hasPrefixOnSameElement = elem.classList.contains(prefix); 288 | const isClassExistsOnParents = elem.customDataSet.prefixes[prefix]; 289 | 290 | if (isClassExistsOnParents && !hasPrefixOnSameElement) { 291 | prefixCorrect = true; 292 | } else { 293 | hasBemWarning = true; 294 | } 295 | 296 | elem.classList.validBem[classItem] = prefixCorrect; 297 | } 298 | 299 | // Check first part of class with -- or _ (modificators) 300 | let modifPrefix = ``; 301 | let modifPrefixCorrect = false; 302 | 303 | if (hasDashesDelimiter) { 304 | modifPrefix = classItem.split(`--`)[0]; 305 | } else if (matchSingleUnderline) { 306 | modifPrefix = classItem.slice(0, matchSingleUnderline.index + 1); 307 | } 308 | 309 | if (modifPrefix) { 310 | if (elem.classList.contains(modifPrefix)) { 311 | modifPrefixCorrect = true; 312 | } else { 313 | hasBemWarning = true; 314 | } 315 | 316 | if (!modifPrefixCorrect) { 317 | elem.classList.validBem[classItem] = modifPrefixCorrect; 318 | } 319 | } 320 | }); 321 | } 322 | 323 | // ------------------------------ 324 | 325 | function addClassesAsPrefixes (elem) { 326 | const classList = elem.classList; 327 | classList.forEach = [].forEach; 328 | 329 | copyPrefixes(elem); 330 | 331 | classList.forEach(function (classItem) { 332 | // Copy only block names 333 | const hasDashesDelimiter = classItem.indexOf(`--`) >= 0; 334 | const hasUnderlinesDelimiter = classItem.indexOf(`__`) >= 0; 335 | const matchSingleUnderline = classItem.match(/[^_]_[^_]/); 336 | 337 | if (!hasUnderlinesDelimiter && 338 | !hasDashesDelimiter && 339 | !matchSingleUnderline 340 | ) { 341 | elem.customDataSet.prefixes[classItem] = classItem; 342 | } 343 | }); 344 | } 345 | 346 | // ------------------------------ 347 | 348 | function copyPrefixes (elem) { 349 | const parent = elem.parentNode; 350 | 351 | if (!parent) { 352 | return; 353 | } 354 | 355 | for (const prefix in parent.customDataSet.prefixes) { 356 | elem.customDataSet.prefixes[prefix] = prefix; 357 | } 358 | } 359 | 360 | // ------------------------------ 361 | 362 | function setRange () { 363 | rangeDeep.max = maxDeep; 364 | rangeDeep.value = maxDeep; 365 | valDeep.innerHTML = maxDeep; 366 | } 367 | 368 | // ------------------------------ 369 | 370 | rangeDeep.oninput = function () { 371 | const level = +this.value; 372 | const styles = `.gnr-level--${level} { display: none }`; 373 | styleElem.innerHTML = styles; 374 | valDeep.innerHTML = this.value; 375 | }; 376 | 377 | // ------------------------------ 378 | 379 | function showCodeErrors () { 380 | showBemMessage(); 381 | checkHeadersLevels(); 382 | printHeadersTree(); 383 | } 384 | 385 | // ------------------------------ 386 | 387 | function showBemMessage () { 388 | bemMessage.classList.toggle(`gnr-hidden`, !hasBemWarning); 389 | } 390 | 391 | // ------------------------------ 392 | 393 | function checkHeadersLevels () { 394 | let isWrongOrder = false; 395 | const realOrder = doc.createElement(`dl`); 396 | realOrder.classList.add(`headers__list`); 397 | let maxUsedHeaders = 0; 398 | let tempHeadersStack = 0; 399 | let longestHeadersStack = 0; 400 | 401 | const realOrderDt = doc.createElement(`dt`); 402 | realOrderDt.classList.add(`headers__title`); 403 | realOrderDt.innerHTML = headersMessageContent.dataset.text; 404 | realOrder.appendChild(realOrderDt); 405 | 406 | for (const key in headersLevels) { 407 | if (headersLevels[key]) { 408 | maxUsedHeaders++; 409 | tempHeadersStack++; 410 | } else { 411 | if (longestHeadersStack < tempHeadersStack) { 412 | longestHeadersStack = tempHeadersStack; 413 | } 414 | tempHeadersStack = 0; 415 | } 416 | } 417 | 418 | if (maxUsedHeaders > longestHeadersStack) { 419 | isWrongOrder = true; 420 | } else if (isWHolePage && !headersLevels.H1) { 421 | isWrongOrder = true; 422 | } 423 | 424 | if (isWrongOrder) { 425 | headersOrder.forEach(function (headerItem) { 426 | const headerItemSpan = doc.createElement(`dd`); 427 | headerItemSpan.classList.add(`headers__item`); 428 | headerItemSpan.innerHTML = headerItem; 429 | 430 | if (headersLevels[headerItem]) { 431 | headerItemSpan.classList.add(`headers__item--found`); 432 | } else { 433 | headerItemSpan.classList.add(`headers__item--notfound`); 434 | } 435 | 436 | realOrder.appendChild(headerItemSpan); 437 | }); 438 | 439 | if (headersMessageContent.firstChild) { 440 | headersMessageContent.removeChild(headersMessageContent.firstChild); 441 | } 442 | headersMessageContent.appendChild(realOrder); 443 | } 444 | 445 | headersMessage.classList.toggle(`gnr-hidden`, !isWrongOrder); 446 | } 447 | 448 | // ------------------------------ 449 | 450 | function printHeadersTree () { 451 | let out = ``; 452 | 453 | if (headersList.length === 0) { 454 | return; 455 | } 456 | 457 | for (let i = 0; i < headersList.length; i++) { 458 | const tag = headersList[i].tagName; 459 | const text = headersList[i].text; 460 | 461 | out += `<${tag}>${tag} ${text}`; 462 | } 463 | 464 | headersMessageTreeContent.innerHTML = out; 465 | headersMessageTree.classList.remove(`gnr-hidden`); 466 | } 467 | 468 | // ------------------------------ 469 | 470 | function checkIsSkippedTag (elem) { 471 | return skippedTags.indexOf(elem.tagName) >= 0; 472 | } 473 | 474 | // ------------------------------ 475 | 476 | function checkIsWholePage (elem) { 477 | if (wholePageMarkers.indexOf(elem.tagName) >= 0) { 478 | isWHolePage = true; 479 | } 480 | } 481 | 482 | // ------------------------------ 483 | 484 | function getTagClass (code, tagName = `body`) { 485 | const regexp = new RegExp(`<${tagName}[^>]*class="(.[^"]*)"`); 486 | const result = code.match(regexp); 487 | 488 | if (result) { 489 | return result[1].split(` `); 490 | } 491 | 492 | return ``; 493 | } 494 | 495 | // ------------------------------ 496 | 497 | // eslint-disable-next-line no-unused-vars 498 | function runDev () { 499 | const testMarkup = ` 500 |

Title

501 |
502 |
503 | 515 |
516 |
517 |
518 |

Title

519 |
Content
520 |
521 |
522 |
523 |
524 |

Title

525 |
Content
526 |
527 |
528 |
529 |
530 | 531 | 535 |
536 |
537 |
538 | `; 539 | codeInput.value = testMarkup; 540 | setHeadersDefs(); 541 | hasBemWarning = false; 542 | createTreeFromHTML(testMarkup); 543 | addClassesActions(); 544 | } 545 | -------------------------------------------------------------------------------- /src/scss/base.scss: -------------------------------------------------------------------------------- 1 | .gnr-html { 2 | font-size: 16px; 3 | } 4 | 5 | .gnr-body { 6 | min-width: 800px; 7 | margin: 0; 8 | background: #EEE; 9 | color: #000; 10 | font: 15px/1.5 Trebuchet MS, sans-serif; 11 | } 12 | 13 | H1, 14 | H2, 15 | H3 { 16 | margin-top: 0; 17 | margin-bottom: .8em; 18 | font-weight: normal; 19 | } 20 | 21 | A { 22 | color: royalblue; 23 | 24 | &:hover { 25 | text-decoration: none; 26 | } 27 | } 28 | 29 | UL, 30 | LI { 31 | padding: 0; 32 | margin: 0; 33 | list-style: none; 34 | } 35 | 36 | .gnr-hidden { 37 | display: none; 38 | } 39 | -------------------------------------------------------------------------------- /src/scss/blocks/code-io.scss: -------------------------------------------------------------------------------- 1 | .gnr-code-input { 2 | width: 100%; 3 | height: 200px; 4 | margin-bottom: 2rem; 5 | padding: .75rem; 6 | box-sizing: border-box; 7 | border: 1px solid #CCC; 8 | box-shadow: 0 0 10px rgba(0, 0, 0, .2) inset; 9 | font: inherit; 10 | line-height: 1.3; 11 | } 12 | 13 | .gnr-code-output { 14 | display: none; 15 | } 16 | -------------------------------------------------------------------------------- /src/scss/blocks/column.scss: -------------------------------------------------------------------------------- 1 | .gnr-column { 2 | width: 48%; 3 | } 4 | 5 | .gnr-column__title { 6 | font-size: 1.5em; 7 | } 8 | -------------------------------------------------------------------------------- /src/scss/blocks/deep-levels.scss: -------------------------------------------------------------------------------- 1 | .gnr-deep__title { 2 | font-size: 1.17em; 3 | } 4 | 5 | .gnr-deep__digit { 6 | display: inline-block; 7 | font-weight: bold; 8 | color: crimson; 9 | } 10 | 11 | .gnr-deep__range { 12 | width: 100%; 13 | margin: 0; 14 | cursor: pointer; 15 | } 16 | -------------------------------------------------------------------------------- /src/scss/blocks/headers-list.scss: -------------------------------------------------------------------------------- 1 | .headers__list { 2 | margin: 0; 3 | display: flex; 4 | flex-wrap: wrap; 5 | } 6 | 7 | .headers__item { 8 | margin-left: .5rem; 9 | 10 | &--notfound { 11 | opacity: .3; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/scss/blocks/headers-tree.scss: -------------------------------------------------------------------------------- 1 | .gnr-message--headers-tree { 2 | margin-top: 2rem; 3 | } 4 | 5 | .gnr-message__content--headers-tree { 6 | background: lighten(yellowgreen, 30%); 7 | font-size: 10px; 8 | 9 | H1, 10 | H2, 11 | H3, 12 | H4, 13 | H5, 14 | H6 { 15 | margin-top: 0; 16 | margin-bottom: .85rem; 17 | line-height: 1.1; 18 | font-weight: bold; 19 | 20 | &:last-child { 21 | margin-bottom: 0; 22 | } 23 | 24 | SPAN { 25 | font-size: .75em; 26 | opacity: .5; 27 | } 28 | } 29 | 30 | /* stylelint-disable no-descending-specificity */ 31 | H1 { 32 | font-size: 2em; 33 | } 34 | 35 | H2 { 36 | margin-left: 1rem; 37 | font-size: 1.75em; 38 | } 39 | 40 | H3 { 41 | margin-left: 3rem; 42 | font-size: 1.5em; 43 | } 44 | 45 | H4 { 46 | margin-left: 4rem; 47 | font-size: 1.25em; 48 | } 49 | 50 | H5 { 51 | margin-left: 5rem; 52 | font-size: 1em; 53 | } 54 | 55 | H6 { 56 | margin-left: 6rem; 57 | font-size: .75em; 58 | } 59 | /* stylelint-enable no-descending-specificity */ 60 | } 61 | -------------------------------------------------------------------------------- /src/scss/blocks/highlight.scss: -------------------------------------------------------------------------------- 1 | @mixin highlight ($color: gold) { 2 | box-shadow: -.2em 0 0 0 $color, 3 | .2em 0 0 0 $color; 4 | background: $color; 5 | } 6 | 7 | .gnr-highlight-bem { 8 | @include highlight; 9 | } 10 | 11 | [data-color="fuchsia"] { 12 | @include highlight( $color: fuchsia ); 13 | } 14 | 15 | [data-color="yellow"] { 16 | @include highlight( $color: yellow ); 17 | } 18 | 19 | [data-color="lime"] { 20 | @include highlight( $color: lime ); 21 | } 22 | 23 | [data-color="aqua"] { 24 | @include highlight( $color: aqua ); 25 | } 26 | -------------------------------------------------------------------------------- /src/scss/blocks/langs.scss: -------------------------------------------------------------------------------- 1 | .gnr-page__langs { 2 | position: absolute; 3 | top: 0; 4 | right: 0; 5 | display: flex; 6 | 7 | A:not([href]) { 8 | font-weight: bold; 9 | color: inherit; 10 | } 11 | } 12 | 13 | .gnr-page__langs-item { 14 | & + & { 15 | margin-left: .5rem; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/scss/blocks/messages.scss: -------------------------------------------------------------------------------- 1 | .gnr-messages { 2 | margin-top: 2rem; 3 | } 4 | 5 | .gnr-message { 6 | margin-top: .75rem; 7 | 8 | &:first-child { 9 | margin-top: 0; 10 | } 11 | } 12 | 13 | .gnr-message__content { 14 | padding: .75rem; 15 | background: lemonchiffon; 16 | } 17 | -------------------------------------------------------------------------------- /src/scss/blocks/tree.scss: -------------------------------------------------------------------------------- 1 | $level-color: lawngreen; 2 | 3 | .gnr-tree { 4 | min-height: 200px; 5 | padding: 1rem; 6 | box-sizing: border-box; 7 | border: 1px solid #DDD; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | } 12 | 13 | .gnr-tree__placeholder { 14 | width: 80%; 15 | text-align: center; 16 | font-size: 14px; 17 | color: #999; 18 | } 19 | 20 | .gnr-tree__content { 21 | max-width: 100%; 22 | } 23 | 24 | /* Level 25 | ------------------------------ */ 26 | 27 | .gnr-level { 28 | position: relative; 29 | margin-bottom: .5rem; 30 | margin-left: 1.2rem; 31 | background: 32 | linear-gradient( 33 | to right, 34 | lawngreen 1px, 35 | transparent 2px 36 | ); 37 | background-repeat: no-repeat; 38 | background-position: 10px 0; 39 | 40 | &::before { 41 | content: ""; 42 | display: block; 43 | position: absolute; 44 | z-index: 0; 45 | left: -1px; 46 | bottom: 0; 47 | width: 2rem; 48 | margin-left: 10px; 49 | height: 10px; 50 | background: white; 51 | border-top: 1px solid $level-color; 52 | } 53 | 54 | &--0::before { 55 | height: 10px; 56 | } 57 | } 58 | 59 | .gnr-level__item { 60 | position: relative; 61 | 62 | &::before { 63 | content: ""; 64 | display: block; 65 | position: absolute; 66 | width: .7rem; 67 | height: .08rem; 68 | margin-left: -.55rem; 69 | margin-top: .6rem; 70 | background: $level-color; 71 | } 72 | 73 | .gnr-level--0 > &::before { 74 | content: none; 75 | } 76 | } 77 | 78 | .gnr-elem { 79 | padding-left: .3rem; 80 | background: white; 81 | } 82 | 83 | .gnr-elem__tag { 84 | background: #FFF; 85 | color: royalblue; 86 | } 87 | 88 | .gnr-class__dot { 89 | position: relative; 90 | padding: 0 .05em; 91 | font-size: 1.2em; 92 | line-height: 1; 93 | } 94 | 95 | .gnr-class__item { 96 | white-space: nowrap; 97 | cursor: pointer; 98 | } 99 | -------------------------------------------------------------------------------- /src/scss/layout.scss: -------------------------------------------------------------------------------- 1 | .gnr-page { 2 | margin: 2rem auto; 3 | padding: 2rem 3rem 3rem; 4 | width: 80%; 5 | min-width: 800px; 6 | max-width: 1200px; 7 | box-sizing: content-box; 8 | background: #FFF; 9 | box-shadow: 0 0 10px rgba(0, 0, 0, .2); 10 | } 11 | 12 | .gnr-page__header { 13 | position: relative; 14 | margin-bottom: 2.65rem; 15 | padding: 1rem 0; 16 | border-bottom: 3px solid #EEE; 17 | } 18 | 19 | .gnr-page__title { 20 | margin-bottom: 0; 21 | text-align: center; 22 | font-size: 2em; 23 | } 24 | 25 | .gnr-page__container { 26 | display: flex; 27 | justify-content: space-between; 28 | } 29 | 30 | .gnr-page__footer { 31 | margin-top: 3rem; 32 | padding-top: 1rem; 33 | border-top: 3px solid #EEE; 34 | display: flex; 35 | justify-content: space-between; 36 | } 37 | -------------------------------------------------------------------------------- /src/scss/styles.scss: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | @import "layout"; 3 | @import "blocks/langs"; 4 | @import "blocks/column"; 5 | @import "blocks/tree"; 6 | @import "blocks/code-io"; 7 | @import "blocks/deep-levels"; 8 | @import "blocks/messages"; 9 | @import "blocks/headers-list"; 10 | @import "blocks/headers-tree"; 11 | @import "blocks/highlight"; 12 | -------------------------------------------------------------------------------- /src/translate/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "lang": "en", 3 | "title": "HTML Tree Generator", 4 | "desc": "Tool shows tree for given HTML markup. As a bonus it checks BEM notation and headers hierarchy.", 5 | "inputCode": "Input HTML", 6 | "levels": "Levels", 7 | "bemError": "It seems like classes use BEM notation, but something went wrong.", 8 | "headers": "Headers", 9 | "headersTree": "Headers tree", 10 | "markupTree": "Markup tree", 11 | "tip": "

To see markup tree, add HTML to textarea on the left.

You can add whole page or particular blocks.

", 12 | "projectOn": "Project on ", 13 | "twitter": "yoksel_en", 14 | "langs": [ 15 | "EN", 16 | "RU" 17 | ], 18 | "path": ".." 19 | } 20 | -------------------------------------------------------------------------------- /src/translate/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "lang": "ru", 3 | "title": "Генератор HTML-дерева", 4 | "desc": "Показывает дерево документа на основе HTML-разметки. Бонусом делается простая проверка БЭМ-классов и иерархии заголовков.", 5 | "inputCode": "Введите код", 6 | "levels": "Уровни", 7 | "bemError": "Похоже, в классах используется БЭМ-нотация, но что-то пошло не так.", 8 | "headers": "Заголовки", 9 | "headersTree": "Структура заголовков", 10 | "markupTree": "Структура разметки", 11 | "tip": "

Чтобы увидеть структуру разметки, вставьте HTML-код в поле слева.

Можно вставлять как отдельные блоки, так и код страницы целиком.

", 12 | "projectOn": "Проект на", 13 | "twitter": "yoksel", 14 | "langs": [ 15 | "EN", 16 | "RU" 17 | ], 18 | "path": "." 19 | } 20 | --------------------------------------------------------------------------------