├── .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 | 
14 |
15 | Бонусом делается простая проверка БЭМ-нотации и иерархии заголовков:
16 |
17 | 
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 | 
32 |
33 | As a bonus there is a simple check of BEM notation and header hierarchy:
34 |
35 | 
36 |
37 | Click on class to highlight it:
38 |
39 |
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 |
13 |
14 |
15 |
16 |
Введите код:
17 |
18 |
19 |
20 |
21 | Уровни:
22 | 1
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | Похоже, в классах используется БЭМ-нотация, но что-то пошло не так.
31 |
32 |
33 |
36 |
37 |
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 + `` + tag + `>`;
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
`;
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 |
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 |
45 |
46 |
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 |
22 |
23 |
24 |
25 |
Введите код:
26 |
27 |
28 |
29 |
30 | Уровни:
31 | 1
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | Похоже, в классах используется БЭМ-нотация, но что-то пошло не так.
40 |
41 |
42 |
45 |
46 |
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 |
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 |
54 |
55 |
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}${tag}>`;
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 |
516 |
517 |
518 |
Title
519 |
Content
520 |
521 |
522 |
523 |
524 |
Title
525 |
Content
526 |
527 |
528 |
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 |
--------------------------------------------------------------------------------