├── .c8rc ├── .clintonrc.json ├── .editorconfig ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── funding.yml └── workflows │ └── nodejs.yml ├── .gitignore ├── .huskyrc ├── .lintstagedrc ├── .npmignore ├── .npmrc ├── ava.config.js ├── changelog.md ├── license ├── package-lock.json ├── package.json ├── readme.md ├── src └── index.ts ├── test ├── templates │ ├── index.html │ ├── parser.ts │ └── render.html └── test-core.spec.ts ├── tsconfig.json └── xo.config.js /.c8rc: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": ["lcov", "text"], 3 | "extension": [".js"] 4 | } -------------------------------------------------------------------------------- /.clintonrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignores": [], 3 | "rules": { 4 | "pkg-main": "off", 5 | "ava": "off", 6 | "xo": "off", 7 | "use-travis": "off" 8 | } 9 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | quote_type = single 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [{*.json,*.yaml,*.yml,*.jade,*.pss,*.css,*.js,*.md,.*,*.ts}] 13 | indent_size = 2 14 | 15 | [{changelog.md,.*}] 16 | insert_final_newline = false 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @voischev @Scrum @mrmlnc @michael-ciniawsky 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | > ✏️ Briefly describe the issue you are experiencing (or the feature you want to see added to the plugin). Tell us what you were trying to do and what happened instead. Remember, this is _not_ a place to ask questions. For that, go to http://gitter.im/posthtml/posthtml 2 | 3 | ### `Details` 4 | 5 | > ✏️ Describe in more detail the problem you have been experiencing, if necessary. 6 | 7 | ## `Error (Logs|Stacks)` 8 | 9 | > 👉 Create a [gist](https://gist.github.com) which is a paste of your **full** logs, and link them here. 10 | 11 | > ⚠️ Do **not** paste your full logs here (or at least hide them by using a `
` block), as it will make this issue long and hard to read! If you are reporting a bug, **always** include logs! 12 | 13 | ### `Reproduction (Code)` 14 | 15 | > ⚠️ Please remember that, with sample code; it's easier to reproduce a bug and much faster to fix it. 16 | 17 | > 🔗 Please refer to a simple code example. 18 | 19 | ```bash 20 | $ git clone https://github.com// 21 | ``` 22 | 23 | ### `Environment` 24 | 25 | > ℹ️ Please provide information about your current environment. 26 | 27 | |OS|node|npm/yarn|package| 28 | |:-:|:--:|:-:|:------:| 29 | |[name][version]|[version]|[version]|[version]| 30 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### `Notable Changes` 2 | 3 | > ✏️ Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue down below. 4 | 5 | #### `Commit Message Summary (CHANGELOG)` 6 | 7 | ``` 8 | commit message body... 9 | ``` 10 | 11 | ### `Type` 12 | 13 | > ℹ️ What types of changes does your code introduce? 14 | 15 | > 👉 _Put an `x` in the boxes that apply and delete all others_ 16 | 17 | - [ ] CI 18 | - [ ] Fix 19 | - [ ] Perf 20 | - [ ] Docs 21 | - [ ] Test 22 | - [ ] Chore 23 | - [ ] Style 24 | - [ ] Build 25 | - [ ] Feature 26 | - [ ] Refactor 27 | 28 | ### `SemVer` 29 | 30 | > ℹ️ What changes to the current `semver` range does your PR introduce? 31 | 32 | > 👉 _Put an `x` in the boxes that apply and delete all others_ 33 | 34 | - [ ] Fix (:label: Patch) 35 | - [ ] Feature (:label: Minor) 36 | - [ ] Breaking Change (:label: Major) 37 | 38 | ### `Issues` 39 | 40 | > ℹ️ What issue(s) (if any) are closed by your PR? 41 | 42 | > 👉 _Replace `#1` with the issue number that applies and remove the ``` ` ```_ 43 | 44 | - Fixes `#1` 45 | 46 | ### `Checklist` 47 | 48 | > ℹ️ You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. This is a reminder of what we are going to look for before merging your code. 49 | 50 | > 👉 _Put an `x` in the boxes that apply and delete all others._ 51 | 52 | - [ ] Lint and unit tests pass with my changes 53 | - [ ] I have added tests that prove my fix is effective/works 54 | - [ ] I have added necessary documentation (if appropriate) 55 | - [ ] Any dependent changes are merged and published in downstream modules 56 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: posthtml 5 | open_collective: posthtml 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Actions Status 2 | on: 3 | pull_request: 4 | types: [opened, synchronize] 5 | branches: 6 | - master 7 | env: 8 | CI: true 9 | 10 | jobs: 11 | run: 12 | name: Node ${{ matrix.node }} on ${{ matrix.os }} 13 | runs-on: ${{ matrix.os }} 14 | 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | node: [12, 14, 16] 19 | os: [ubuntu-latest] 20 | 21 | steps: 22 | - name: Clone repository 23 | uses: actions/checkout@v2 24 | 25 | - name: Set Node.js version 26 | uses: actions/setup-node@v1 27 | with: 28 | node-version: ${{ matrix.node }} 29 | 30 | - name: Install npm dependencies 31 | run: npm ci 32 | 33 | - name: Run tests 34 | run: npm run test 35 | 36 | - name: Run Coveralls 37 | uses: coverallsapp/github-action@master 38 | with: 39 | github-token: "${{ secrets.GITHUB_TOKEN }}" 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | npm-debug.log 5 | dist -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-push": "npm t", 4 | "commit-msg": "commitlint --extends=@commitlint/config-angular -e" 5 | } 6 | } -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.js": "xo" 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .gitignore 3 | .travis.yml 4 | coverage 5 | .nyc_output 6 | src -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ava.config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | verbose: true, 3 | timeout: '1m', 4 | files: ['test/test-*'], 5 | extensions: ['ts'], 6 | require: ['esm', 'esbuild-register'], 7 | }; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ## 3.0.0 (2021-07-27) 2 | 3 | * style: after lint ([d3d9d8b](https://github.com/posthtml/posthtml-render/commit/d3d9d8b)) 4 | * style: indent ([3d1fe36](https://github.com/posthtml/posthtml-render/commit/3d1fe36)) 5 | * build: update dep dev ([332eef4](https://github.com/posthtml/posthtml-render/commit/332eef4)) 6 | * docs: update usage ([d80591f](https://github.com/posthtml/posthtml-render/commit/d80591f)) 7 | * refactor: export as not default render, close #63 ([f45d8ed](https://github.com/posthtml/posthtml-render/commit/f45d8ed)), closes [#63](https://github.com/posthtml/posthtml-render/issues/63) 8 | * perf: move types to index ([7431c0f](https://github.com/posthtml/posthtml-render/commit/7431c0f)) 9 | * perf: remove trash ([fa85549](https://github.com/posthtml/posthtml-render/commit/fa85549)) 10 | * fix: referenced incorrectly in package.json close #62 ([d0bd697](https://github.com/posthtml/posthtml-render/commit/d0bd697)), closes [#62](https://github.com/posthtml/posthtml-render/issues/62) 11 | 12 | 13 | 14 | ## 2.0.6 (2021-06-11) 15 | 16 | * 2.0.6 ([aa66af1](https://github.com/posthtml/posthtml-render/commit/aa66af1)) 17 | * style: ignore ts error ([aaab81d](https://github.com/posthtml/posthtml-render/commit/aaab81d)) 18 | * fix: incorrect detect json format ([c334ef2](https://github.com/posthtml/posthtml-render/commit/c334ef2)) 19 | * fix: is-json not declare ([85b7bec](https://github.com/posthtml/posthtml-render/commit/85b7bec)) 20 | * build: update depdev ([76c0752](https://github.com/posthtml/posthtml-render/commit/76c0752)) 21 | * test: change to number with quote ([e2f6cb5](https://github.com/posthtml/posthtml-render/commit/e2f6cb5)) 22 | 23 | 24 | 25 | ## 2.0.5 (2021-06-10) 26 | 27 | * 2.0.5 ([efc4a41](https://github.com/posthtml/posthtml-render/commit/efc4a41)) 28 | * test: for long test ([d019534](https://github.com/posthtml/posthtml-render/commit/d019534)) 29 | * test: invalid in htmlnano travis ([69d666b](https://github.com/posthtml/posthtml-render/commit/69d666b)) 30 | * test: more tests ([4dfa92e](https://github.com/posthtml/posthtml-render/commit/4dfa92e)) 31 | * test: off only one test ([fcdda46](https://github.com/posthtml/posthtml-render/commit/fcdda46)) 32 | * fix: empty and false content, close #58 ([af9d828](https://github.com/posthtml/posthtml-render/commit/af9d828)), closes [#58](https://github.com/posthtml/posthtml-render/issues/58) 33 | 34 | 35 | 36 | ## 2.0.4 (2021-06-08) 37 | 38 | * 2.0.4 ([698e31f](https://github.com/posthtml/posthtml-render/commit/698e31f)) 39 | * perf: engine up to 12 version ([7c430be](https://github.com/posthtml/posthtml-render/commit/7c430be)) 40 | * fix: if tree contains empty string, close #58 ([401f5a6](https://github.com/posthtml/posthtml-render/commit/401f5a6)), closes [#58](https://github.com/posthtml/posthtml-render/issues/58) 41 | * test: if tree exists empty string, issue #58 ([3493d4f](https://github.com/posthtml/posthtml-render/commit/3493d4f)), closes [#58](https://github.com/posthtml/posthtml-render/issues/58) 42 | 43 | 44 | 45 | ## 2.0.3 (2021-06-04) 46 | 47 | * 2.0.3 ([f5cd69d](https://github.com/posthtml/posthtml-render/commit/f5cd69d)) 48 | * test: if in content has empty array, issue #56 ([95bad06](https://github.com/posthtml/posthtml-render/commit/95bad06)), closes [#56](https://github.com/posthtml/posthtml-render/issues/56) 49 | * fix: if in content has empty array, close #56 ([892d602](https://github.com/posthtml/posthtml-render/commit/892d602)), closes [#56](https://github.com/posthtml/posthtml-render/issues/56) 50 | 51 | 52 | 53 | ## 2.0.2 (2021-06-03) 54 | 55 | * 2.0.2 ([bcbdc60](https://github.com/posthtml/posthtml-render/commit/bcbdc60)) 56 | * fix: concate single tag ([a6764cf](https://github.com/posthtml/posthtml-render/commit/a6764cf)) 57 | * test: fix only one test ([60f598d](https://github.com/posthtml/posthtml-render/commit/60f598d)) 58 | 59 | 60 | 61 | ## 2.0.1 (2021-05-24) 62 | 63 | * 2.0.1 ([feb426c](https://github.com/posthtml/posthtml-render/commit/feb426c)) 64 | * ci: drop support old node ([810c46c](https://github.com/posthtml/posthtml-render/commit/810c46c)) 65 | * ci: lock ([3c894d6](https://github.com/posthtml/posthtml-render/commit/3c894d6)) 66 | * build: add prepare script, close #53 ([dbc9b47](https://github.com/posthtml/posthtml-render/commit/dbc9b47)), closes [#53](https://github.com/posthtml/posthtml-render/issues/53) 67 | * test: for string template ([1047ce5](https://github.com/posthtml/posthtml-render/commit/1047ce5)) 68 | * docs: typo ([fae9dad](https://github.com/posthtml/posthtml-render/commit/fae9dad)) 69 | 70 | 71 | 72 | ## 1.3.1 (2021-03-12) 73 | 74 | * 1.3.1 ([b59d962](https://github.com/posthtml/posthtml-render/commit/b59d962)) 75 | * build: update dep dev ([acf654b](https://github.com/posthtml/posthtml-render/commit/acf654b)) 76 | * test: Case sensitivity breaks empty elements, issue #49 ([041774c](https://github.com/posthtml/posthtml-render/commit/041774c)), closes [#49](https://github.com/posthtml/posthtml-render/issues/49) 77 | * test: Case sensitivity breaks empty elements, issue #49 ([16b00d4](https://github.com/posthtml/posthtml-render/commit/16b00d4)), closes [#49](https://github.com/posthtml/posthtml-render/issues/49) 78 | * test: remove undefined write ([9d22fff](https://github.com/posthtml/posthtml-render/commit/9d22fff)) 79 | * fix: case sensitivity breaks empty elements, close #49 ([1a87466](https://github.com/posthtml/posthtml-render/commit/1a87466)), closes [#49](https://github.com/posthtml/posthtml-render/issues/49) 80 | * docs: remove unused badges ([903037c](https://github.com/posthtml/posthtml-render/commit/903037c)) 81 | 82 | 83 | 84 | ## 2.0.0 (2021-05-07) 85 | 86 | * 2.0.0 ([3c898d4](https://github.com/posthtml/posthtml-render/commit/3c898d4)) 87 | * docs: description node property closeAs, issue #48 ([5979fc9](https://github.com/posthtml/posthtml-render/commit/5979fc9)), closes [#48](https://github.com/posthtml/posthtml-render/issues/48) 88 | * type: add node property closeAs, issue #48 ([00ec9b0](https://github.com/posthtml/posthtml-render/commit/00ec9b0)), closes [#48](https://github.com/posthtml/posthtml-render/issues/48) 89 | * test: JSON strings in data attributes, #46 ([d0508e2](https://github.com/posthtml/posthtml-render/commit/d0508e2)), closes [#46](https://github.com/posthtml/posthtml-render/issues/46) 90 | * test: node property closeAs, issue #48 ([f561e6b](https://github.com/posthtml/posthtml-render/commit/f561e6b)), closes [#48](https://github.com/posthtml/posthtml-render/issues/48) 91 | * feat: add node property closeAs, close #48 ([6892ede](https://github.com/posthtml/posthtml-render/commit/6892ede)), closes [#48](https://github.com/posthtml/posthtml-render/issues/48) 92 | * feat: JSON strings in data attributes, close #46 ([5539187](https://github.com/posthtml/posthtml-render/commit/5539187)), closes [#46](https://github.com/posthtml/posthtml-render/issues/46) 93 | * merge: from master ([3ab5e82](https://github.com/posthtml/posthtml-render/commit/3ab5e82)) 94 | * refactor: migrate to typescript ([9eaaca4](https://github.com/posthtml/posthtml-render/commit/9eaaca4)) 95 | 96 | 97 | 98 | ## 1.3.1 (2021-03-12) 99 | 100 | * 1.3.1 ([b59d962](https://github.com/posthtml/posthtml-render/commit/b59d962)) 101 | * Delete test.html ([0dea643](https://github.com/posthtml/posthtml-render/commit/0dea643)) 102 | * Update funding.yml ([86f2c9d](https://github.com/posthtml/posthtml-render/commit/86f2c9d)) 103 | * build: update dep dev ([acf654b](https://github.com/posthtml/posthtml-render/commit/acf654b)) 104 | * test: Case sensitivity breaks empty elements, issue #49 ([041774c](https://github.com/posthtml/posthtml-render/commit/041774c)), closes [#49](https://github.com/posthtml/posthtml-render/issues/49) 105 | * test: Case sensitivity breaks empty elements, issue #49 ([16b00d4](https://github.com/posthtml/posthtml-render/commit/16b00d4)), closes [#49](https://github.com/posthtml/posthtml-render/issues/49) 106 | * test: quoteStyle option ([18d31c0](https://github.com/posthtml/posthtml-render/commit/18d31c0)) 107 | * test: remove undefined write ([9d22fff](https://github.com/posthtml/posthtml-render/commit/9d22fff)) 108 | * fix: case sensitivity breaks empty elements, close #49 ([1a87466](https://github.com/posthtml/posthtml-render/commit/1a87466)), closes [#49](https://github.com/posthtml/posthtml-render/issues/49) 109 | * docs: add quoteStyle option ([38b2b04](https://github.com/posthtml/posthtml-render/commit/38b2b04)) 110 | * docs: remove unused badges ([903037c](https://github.com/posthtml/posthtml-render/commit/903037c)) 111 | * feat: add type definition for quoteStyle ([3ee8d58](https://github.com/posthtml/posthtml-render/commit/3ee8d58)) 112 | * feat: implement quoteStyle option ([903300f](https://github.com/posthtml/posthtml-render/commit/903300f)) 113 | 114 | 115 | 116 | ## 1.3.0 (2020-11-12) 117 | 118 | * 1.3.0 ([334705d](https://github.com/posthtml/posthtml-render/commit/334705d)) 119 | * ci: package lock for ci ([4c85131](https://github.com/posthtml/posthtml-render/commit/4c85131)) 120 | * ci: remove windows ([34aa3c2](https://github.com/posthtml/posthtml-render/commit/34aa3c2)) 121 | * docs: add description for replaceQuote ([44ab8b0](https://github.com/posthtml/posthtml-render/commit/44ab8b0)) 122 | * style: after lint ([c944f82](https://github.com/posthtml/posthtml-render/commit/c944f82)) 123 | * reaftor: build sustem ([b63ec4e](https://github.com/posthtml/posthtml-render/commit/b63ec4e)) 124 | * feat: replaceQuote, close #43 ([00927c3](https://github.com/posthtml/posthtml-render/commit/00927c3)), closes [#43](https://github.com/posthtml/posthtml-render/issues/43) 125 | * test: not replace quote in arribute, issue #43 ([ccb7d23](https://github.com/posthtml/posthtml-render/commit/ccb7d23)), closes [#43](https://github.com/posthtml/posthtml-render/issues/43) 126 | 127 | 128 | 129 | ## 1.2.3 (2020-07-28) 130 | 131 | * chore: adds typescript type definition ([8e69f1d](https://github.com/posthtml/posthtml-render/commit/8e69f1d)) 132 | * chore(release): 1.2.3 ([397e00b](https://github.com/posthtml/posthtml-render/commit/397e00b)) 133 | * build(deps-dev): bump standard-version from 7.1.0 to 8.0.1 ([fe1be76](https://github.com/posthtml/posthtml-render/commit/fe1be76)) 134 | * build(package): update dep dev ([98281b2](https://github.com/posthtml/posthtml-render/commit/98281b2)) 135 | 136 | 137 | 138 | ## 1.2.2 (2020-04-16) 139 | 140 | * chore(release): 1.2.2 ([2f88db9](https://github.com/posthtml/posthtml-render/commit/2f88db9)) 141 | * revert: because yarn by default respects the engine ([1ff7911](https://github.com/posthtml/posthtml-render/commit/1ff7911)) 142 | 143 | 144 | 145 | ## 1.2.1 (2020-04-14) 146 | 147 | * chore(release): 1.2.1 ([dd09a17](https://github.com/posthtml/posthtml-render/commit/dd09a17)) 148 | * fix: illegal addition of custom tags ([f936272](https://github.com/posthtml/posthtml-render/commit/f936272)) 149 | * docs: add keeping tags unquoted ([35a3c9d](https://github.com/posthtml/posthtml-render/commit/35a3c9d)) 150 | * docs: fix node url ([0049dca](https://github.com/posthtml/posthtml-render/commit/0049dca)) 151 | * build: frop support old node ([714f42f](https://github.com/posthtml/posthtml-render/commit/714f42f)) 152 | * ci: drop support old node ([5f6bef8](https://github.com/posthtml/posthtml-render/commit/5f6bef8)) 153 | 154 | 155 | 156 | ## 1.2.0 (2020-02-25) 157 | 158 | * chore: 100% coveralls ([3ede092](https://github.com/posthtml/posthtml-render/commit/3ede092)) 159 | * chore(release): 1.2.0 ([5d53868](https://github.com/posthtml/posthtml-render/commit/5d53868)) 160 | * style: after lint ([bb22948](https://github.com/posthtml/posthtml-render/commit/bb22948)) 161 | * build: update dep dev ([53a7af4](https://github.com/posthtml/posthtml-render/commit/53a7af4)) 162 | * Add support for keeping tags unquoted. ([3492483](https://github.com/posthtml/posthtml-render/commit/3492483)) 163 | * Fix rendering of unquoted empty attributes. ([3d1d99c](https://github.com/posthtml/posthtml-render/commit/3d1d99c)) 164 | * perf: extra performance ([f118d98](https://github.com/posthtml/posthtml-render/commit/f118d98)) 165 | * docs: fix node support badges ([53d7464](https://github.com/posthtml/posthtml-render/commit/53d7464)) 166 | 167 | 168 | 169 | ## 1.1.5 (2019-05-06) 170 | 171 | * chore(release): 1.1.5 ([c9817b4](https://github.com/posthtml/posthtml-render/commit/c9817b4)) 172 | * ci: change script to run coveralls ([860f58b](https://github.com/posthtml/posthtml-render/commit/860f58b)) 173 | * ci: try fix coveralls ([2f69283](https://github.com/posthtml/posthtml-render/commit/2f69283)) 174 | * ci: try fix coveralls ([e615e69](https://github.com/posthtml/posthtml-render/commit/e615e69)) 175 | * build: add lint pretest ([bece65d](https://github.com/posthtml/posthtml-render/commit/bece65d)) 176 | * build: update depDev ([f6a6b85](https://github.com/posthtml/posthtml-render/commit/f6a6b85)) 177 | * style: according standard ([85f914a](https://github.com/posthtml/posthtml-render/commit/85f914a)) 178 | * perf: drop support old nodejs ([4663c4d](https://github.com/posthtml/posthtml-render/commit/4663c4d)) 179 | * fix: cut out content, close #25 ([03acfa8](https://github.com/posthtml/posthtml-render/commit/03acfa8)), closes [#25](https://github.com/posthtml/posthtml-render/issues/25) 180 | * test: fail with options closingSingleTag slash, #25 ([8be0ded](https://github.com/posthtml/posthtml-render/commit/8be0ded)), closes [#25](https://github.com/posthtml/posthtml-render/issues/25) 181 | * Fix readme section about 'closingSingleTag' ([ce4144f](https://github.com/posthtml/posthtml-render/commit/ce4144f)) 182 | 183 | 184 | 185 | ## 1.1.4 (2018-05-11) 186 | 187 | * chore(release): 1.1.4 ([7999cc8](https://github.com/posthtml/posthtml-render/commit/7999cc8)) 188 | * Add test on double quotes in attribute values ([cd277a5](https://github.com/posthtml/posthtml-render/commit/cd277a5)) 189 | * Fix rendering double quotes in html attributes ([3a6eb19](https://github.com/posthtml/posthtml-render/commit/3a6eb19)) 190 | 191 | 192 | 193 | ## 1.1.3 (2018-04-04) 194 | 195 | * chore(CODEOWNERS): fix username (`@GitScrum` => `@Scrum`) ([f9f9c0a](https://github.com/posthtml/posthtml-render/commit/f9f9c0a)) 196 | * chore(release): 1.1.3 ([bbc4e73](https://github.com/posthtml/posthtml-render/commit/bbc4e73)) 197 | * fix(lib/index): don't handle `` as self-closing tag ([c48a2e2](https://github.com/posthtml/posthtml-render/commit/c48a2e2)) 198 | 199 | 200 | 201 | ## 1.1.2 (2018-03-20) 202 | 203 | * chore(package): remove clean script - not use ([df85eb0](https://github.com/posthtml/posthtml-render/commit/df85eb0)) 204 | * chore(package): remove run script build in release script ([7e8c096](https://github.com/posthtml/posthtml-render/commit/7e8c096)) 205 | * chore(release): 1.1.2 ([4cd2b4a](https://github.com/posthtml/posthtml-render/commit/4cd2b4a)) 206 | * delete browser.min.js ([c1d766b](https://github.com/posthtml/posthtml-render/commit/c1d766b)) 207 | * remove `browser` ([658ef38](https://github.com/posthtml/posthtml-render/commit/658ef38)) 208 | 209 | 210 | 211 | ## 1.1.1 (2018-03-02) 212 | 213 | * chore(.editorconfig): use 2 spaces as `indent_size` ([7359ae4](https://github.com/posthtml/posthtml-render/commit/7359ae4)) 214 | * chore(.github): add `CODEOWNERS` ([0270bb6](https://github.com/posthtml/posthtml-render/commit/0270bb6)) 215 | * chore(.github): add `ISSUE_TEMPLATE` ([25fcd58](https://github.com/posthtml/posthtml-render/commit/25fcd58)) 216 | * chore(.github): add `PULL_REQUEST_TEMPLATE` ([94416b8](https://github.com/posthtml/posthtml-render/commit/94416b8)) 217 | * chore(.gitignore): add `nyc_output` ([ed05dda](https://github.com/posthtml/posthtml-render/commit/ed05dda)) 218 | * chore(.npmignore): add `.nyc_output` && coverage ([0ca896d](https://github.com/posthtml/posthtml-render/commit/0ca896d)) 219 | * chore(.npmrc): don't generate a lockfile ([4674906](https://github.com/posthtml/posthtml-render/commit/4674906)) 220 | * chore(package): update dependencies ([ad8f1d4](https://github.com/posthtml/posthtml-render/commit/ad8f1d4)) 221 | * chore(release): 1.1.1 ([0361296](https://github.com/posthtml/posthtml-render/commit/0361296)) 222 | * style: fix lint report ([4539ef5](https://github.com/posthtml/posthtml-render/commit/4539ef5)) 223 | * style: use `standard` ([90b29ea](https://github.com/posthtml/posthtml-render/commit/90b29ea)) 224 | * refactor(lib): remove module wrapper && minor cleanups ([aed12f3](https://github.com/posthtml/posthtml-render/commit/aed12f3)) 225 | * test: refactor ([54562b4](https://github.com/posthtml/posthtml-render/commit/54562b4)) 226 | * build(rollup.config.js): use `rollup` for browser builds ([0b17496](https://github.com/posthtml/posthtml-render/commit/0b17496)) 227 | * docs(LICENSE): update year ([cb3b09e](https://github.com/posthtml/posthtml-render/commit/cb3b09e)) 228 | * docs(README): standardize ([9daa9e0](https://github.com/posthtml/posthtml-render/commit/9daa9e0)) 229 | * docs(RENDER): init JSDoc ([8066597](https://github.com/posthtml/posthtml-render/commit/8066597)) 230 | * ci(.travis): add node `stable` && `lts` ([f783b90](https://github.com/posthtml/posthtml-render/commit/f783b90)) 231 | 232 | 233 | 234 | ## 1.1.0 (2018-01-18) 235 | 236 | * v1.1.0 ([488fb69](https://github.com/posthtml/posthtml-render/commit/488fb69)) 237 | * build: rebuild mini ([53bbf3a](https://github.com/posthtml/posthtml-render/commit/53bbf3a)) 238 | * feat: allow regexps in singleTags option ([e4308c9](https://github.com/posthtml/posthtml-render/commit/e4308c9)) 239 | 240 | 241 | 242 | ## 1.0.7 (2018-01-18) 243 | 244 | * Create MAINTAINERS ([2960f4b](https://github.com/posthtml/posthtml-render/commit/2960f4b)) 245 | * Update README.md ([0948079](https://github.com/posthtml/posthtml-render/commit/0948079)) 246 | * v1.0.7 ([508e094](https://github.com/posthtml/posthtml-render/commit/508e094)) 247 | * ci: try fix ([0719812](https://github.com/posthtml/posthtml-render/commit/0719812)) 248 | * jsdoc: parse -> render ([a8f5d5d](https://github.com/posthtml/posthtml-render/commit/a8f5d5d)) 249 | 250 | 251 | 252 | ## 1.0.6 (2016-02-29) 253 | 254 | * 1.0.6 ([f72923c](https://github.com/posthtml/posthtml-render/commit/f72923c)) 255 | * fix immutable tree obj ([485e7fe](https://github.com/posthtml/posthtml-render/commit/485e7fe)) 256 | 257 | 258 | 259 | ## 1.0.5 (2015-12-18) 260 | 261 | * 1.0.5 ([2a81c32](https://github.com/posthtml/posthtml-render/commit/2a81c32)) 262 | * fix render empty string attrs keys ([c47064b](https://github.com/posthtml/posthtml-render/commit/c47064b)) 263 | 264 | 265 | 266 | ## 1.0.4 (2015-11-29) 267 | 268 | * 1.0.4 ([7557c70](https://github.com/posthtml/posthtml-render/commit/7557c70)) 269 | * fix when array in content ([12bde13](https://github.com/posthtml/posthtml-render/commit/12bde13)) 270 | 271 | 272 | 273 | ## 1.0.3 (2015-10-25) 274 | 275 | * 1.0.3 ([afa7021](https://github.com/posthtml/posthtml-render/commit/afa7021)) 276 | * add npm scripts ([820d005](https://github.com/posthtml/posthtml-render/commit/820d005)) 277 | * fix render empty key attrs & number attrs key ([7a74dd6](https://github.com/posthtml/posthtml-render/commit/7a74dd6)) 278 | 279 | 280 | 281 | ## 1.0.2 (2015-10-21) 282 | 283 | * no render false attrs ([0c76236](https://github.com/posthtml/posthtml-render/commit/0c76236)) 284 | * Release v1.0.2 ([7faa900](https://github.com/posthtml/posthtml-render/commit/7faa900)) 285 | 286 | 287 | 288 | ## 1.0.1 (2015-10-20) 289 | 290 | * fix readme after transfer ([0dd2fdd](https://github.com/posthtml/posthtml-render/commit/0dd2fdd)) 291 | * more tests ([f061c65](https://github.com/posthtml/posthtml-render/commit/f061c65)) 292 | * Release v1.0.1 ([02674c2](https://github.com/posthtml/posthtml-render/commit/02674c2)) 293 | * render number & fix skip tag false ([0dce005](https://github.com/posthtml/posthtml-render/commit/0dce005)) 294 | 295 | 296 | 297 | ## 1.0.0 (2015-10-19) 298 | 299 | * add badge ([bf092f4](https://github.com/posthtml/posthtml-render/commit/bf092f4)) 300 | * add module wrapper ([36f506b](https://github.com/posthtml/posthtml-render/commit/36f506b)) 301 | * add package.json ([0837e12](https://github.com/posthtml/posthtml-render/commit/0837e12)) 302 | * basic tests ([cffae42](https://github.com/posthtml/posthtml-render/commit/cffae42)) 303 | * Initial commit ([18bd42f](https://github.com/posthtml/posthtml-render/commit/18bd42f)) 304 | * Release 1.0.0 ([35699e6](https://github.com/posthtml/posthtml-render/commit/35699e6)) 305 | * upd jscs config ([64b9f1a](https://github.com/posthtml/posthtml-render/commit/64b9f1a)) 306 | * upd Readme & add License file ([6036ea9](https://github.com/posthtml/posthtml-render/commit/6036ea9)) 307 | * feat(*): tests ([7ca52d5](https://github.com/posthtml/posthtml-render/commit/7ca52d5)) 308 | 309 | 310 | 311 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | License (MIT) 2 | 3 | Copyright (c) 2017 Ivan Voischev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "posthtml-render", 3 | "version": "3.0.0", 4 | "description": "Renders PostHTML Tree to HTML/XML", 5 | "license": "MIT", 6 | "repository": "posthtml/posthtml-render", 7 | "author": "Ivan Voischev ", 8 | "main": "dist/index.js", 9 | "engines": { 10 | "node": ">=12" 11 | }, 12 | "scripts": { 13 | "version": "conventional-changelog -i changelog.md -s -r 0 && git add changelog.md", 14 | "build": "rm -rf dist && tsup src/*.ts --dts --minify", 15 | "dev": "npm run build -- --watch", 16 | "test": "xo && c8 ava", 17 | "pretest": "clinton", 18 | "prepare": "npm run build" 19 | }, 20 | "files": [ 21 | "dist" 22 | ], 23 | "keywords": [ 24 | "posthtml", 25 | "render", 26 | "html", 27 | "xml" 28 | ], 29 | "dependencies": { 30 | "is-json": "^2.0.1" 31 | }, 32 | "devDependencies": { 33 | "@antfu/eslint-config-ts": "^0.7.0", 34 | "@commitlint/cli": "^13.1.0", 35 | "@commitlint/config-angular": "^13.1.0", 36 | "@types/node": "^16.4.3", 37 | "ava": "^3.13.0", 38 | "c8": "^7.7.3", 39 | "clinton": "^0.14.0", 40 | "conventional-changelog-cli": "^2.0.34", 41 | "esbuild-register": "^2.6.0", 42 | "eslint": "^7.31.0", 43 | "esm": "^3.2.25", 44 | "husky": "^7.0.1", 45 | "lint-staged": "^11.1.1", 46 | "posthtml-parser": "^0.9.1", 47 | "rewire": "^5.0.0", 48 | "rimraf": "^3.0.0", 49 | "ts-node": "^10.1.0", 50 | "tsup": "^4.12.5", 51 | "typescript": "^4.3.5", 52 | "xo": "^0.42.0" 53 | }, 54 | "types": "dist/index.d.ts" 55 | } 56 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![npm][npm]][npm-url] 2 | [![node]]() 3 | [![tests][tests]][tests-url] 4 | [![coverage][cover]][cover-url] 5 | 6 |
7 | 8 | PostHTML 10 | 11 |

PostHTML Render

12 |

Renders a PostHTML Tree to HTML/XML

13 |
14 | 15 |

Install

16 | 17 | ```bash 18 | npm i -D posthtml-render 19 | ``` 20 | 21 |

Usage

22 | 23 | ### `NodeJS` 24 | 25 | ```js 26 | import { render } from 'posthtml-render'; 27 | 28 | const tree = []; 29 | 30 | const node = {}; 31 | 32 | node.tag = 'ul'; 33 | node.attrs = { class: 'list' }; 34 | node.content = [ 35 | 'one', 36 | 'two', 37 | 'three' 38 | ].map((content) => ({ tag: 'li', content })); 39 | 40 | tree.push(node); 41 | 42 | const html = render(tree, options); 43 | 44 | ``` 45 | 46 | ```html 47 |
    48 |
  • one
  • 49 |
  • two
  • 50 |
  • three
  • 51 |
52 | ``` 53 | 54 |

Options

55 | 56 | |Name|Type|Default|Description| 57 | |:--:|:--:|:-----:|:----------| 58 | |**[`singleTags`](#singletags)**|`{Array}`|`[]`|Specify custom single tags (self closing)| 59 | |**[`closingSingleTag`](#closingSingleTag)**|`{String}`|[`>`](#default)|Specify the single tag closing format| 60 | |**[`quoteAllAttributes`](#quoteAllAttributes)**|`{Boolean}`|[`true`](#default)|Put double quotes around all tags, even when not necessary.| 61 | |**[`replaceQuote`](#replaceQuote)**|`{Boolean}`|[`true`](#default)|Replaces quotes in attribute values with `"e;`.| 62 | |**[`quoteStyle`](#quoteStyle)**|`{0 or 1 or 2}`|[`2`](#default)|Specify the style of quote arround the attribute values| 63 | 64 | ### `singleTags` 65 | 66 | Specify custom single tags (self closing) 67 | 68 | ### `{String}` 69 | 70 | ```js 71 | const render = require('posthtml-render') 72 | 73 | const tree = [ { tag: 'name' } ] 74 | const options = { singleTags: [ 'name' ] } 75 | 76 | const html = render(tree, options) 77 | ``` 78 | 79 | **result.html** 80 | ```html 81 | 82 | ``` 83 | 84 | #### `{RegExp}` 85 | 86 | ```js 87 | const render = require('posthtml-render') 88 | 89 | const tree = [ { tag: '%=title%' } ] 90 | const options = { singleTags: [ /^%.*%$/ ] } 91 | 92 | const html = render(tree, options) 93 | ``` 94 | 95 | **result.html** 96 | ```html 97 | <%=title%> 98 | ``` 99 | 100 | ### `closingSingleTag` 101 | 102 | Specify the single tag closing format 103 | 104 | #### `Formats` 105 | 106 | ```js 107 | const render = require('posthtml-render') 108 | 109 | const tree = [ { tag: 'img' } ] 110 | ``` 111 | 112 | ##### `'tag'` 113 | 114 | ```js 115 | const html = render(tree, { closingSingleTag: 'tag' }) 116 | ``` 117 | 118 | ```html 119 | 120 | ``` 121 | 122 | ##### `'slash'` 123 | 124 | ```js 125 | const html = render(tree, { closingSingleTag: 'slash' }) 126 | ``` 127 | 128 | ```html 129 | 130 | ``` 131 | 132 | ##### `'default' (Default)` 133 | 134 | ```js 135 | const html = render(tree) 136 | ``` 137 | 138 | ```html 139 | 140 | ``` 141 | 142 | ##### `'closeAs'` 143 | 144 | ```js 145 | const tree = [ { 146 | tag: 'custom', 147 | closeAs: 'default' // Available types: `tag` | `slash` | `default` 148 | } ] 149 | const html = render(tree, { closingSingleTag: 'closeAs' }) 150 | ``` 151 | 152 | ```html 153 | 154 | ``` 155 | 156 | ### `quoteAllAttributes` 157 | 158 | Specify if all attributes should be quoted. 159 | 160 | ##### `true (Default)` 161 | 162 | ```html 163 | 164 | ``` 165 | 166 | ##### `false` 167 | 168 | ```html 169 | 170 | ``` 171 | 172 | ### `replaceQuote` 173 | 174 | Replaces quotes in attribute values with `"e;`. 175 | 176 | ##### `true (Default)` 177 | 178 | ```html 179 | 180 | ``` 181 | 182 | ##### `false` 183 | 184 | ```html 185 | "> 186 | ``` 187 | 188 | ### `quoteStyle` 189 | 190 | ##### `2 (Default)` 191 | 192 | Attribute values are wrapped in double quotes: 193 | 194 | ```html 195 | 196 | ``` 197 | 198 | ##### `1` 199 | 200 | Attribute values are wrapped in single quote: 201 | 202 | ```html 203 | 204 | ``` 205 | 206 | ##### `0` 207 | 208 | Quote style is based on attribute values (an alternative for `replaceQuote` option): 209 | 210 | ```html 211 | 212 | ``` 213 | 214 | 215 | [npm]: https://img.shields.io/npm/v/posthtml-render.svg 216 | [npm-url]: https://npmjs.com/package/posthtml-render 217 | 218 | [node]: https://img.shields.io/node/v/posthtml-render.svg 219 | [node-url]: https://img.shields.io/node/v/posthtml-render.svg 220 | 221 | [tests]: http://img.shields.io/travis/posthtml/posthtml-render.svg 222 | [tests-url]: https://travis-ci.org/posthtml/posthtml-render 223 | 224 | [cover]: https://coveralls.io/repos/github/posthtml/posthtml-render/badge.svg 225 | [cover-url]: https://coveralls.io/github/posthtml/posthtml-render 226 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error 2 | import isJSON from 'is-json'; 3 | import {Attributes, NodeText, NodeTag} from 'posthtml-parser'; 4 | 5 | export enum quoteStyleEnum { 6 | Smart, 7 | Single, 8 | Double, 9 | } 10 | 11 | export enum closingSingleTagOptionEnum { 12 | tag = 'tag', 13 | slash = 'slash', 14 | default = 'default', 15 | closeAs = 'closeAs', 16 | } 17 | 18 | export enum closingSingleTagTypeEnum { 19 | tag = 'tag', 20 | slash = 'slash', 21 | default = 'default', 22 | } 23 | 24 | export type Node = NodeText | NodeTag & { 25 | closeAs?: closingSingleTagTypeEnum; 26 | }; 27 | 28 | export type Options = { 29 | /** 30 | * Custom single tags (selfClosing). 31 | * 32 | * @default [] 33 | */ 34 | singleTags?: Array; 35 | 36 | /** 37 | * Closing format for single tag. 38 | * 39 | * Formats: 40 | * 41 | * tag: `

`, slash: `
`, default: `
` 42 | * 43 | */ 44 | closingSingleTag?: closingSingleTagOptionEnum; 45 | 46 | /** 47 | * If all attributes should be quoted. 48 | * Otherwise attributes will be unquoted when allowed. 49 | * 50 | * @default true 51 | */ 52 | quoteAllAttributes?: boolean; 53 | 54 | /** 55 | * Replaces quotes in attribute values with `"e;`. 56 | * 57 | * @default true 58 | */ 59 | replaceQuote?: boolean; 60 | 61 | /** 62 | * Quote style 63 | * 64 | * 0 - Smart quotes 65 | * 66 | * 1 - Single quotes 67 | * 68 | * 2 - double quotes 69 | * 70 | * 71 | * @default 2 72 | */ 73 | quoteStyle?: quoteStyleEnum; 74 | }; 75 | 76 | const SINGLE_TAGS: Array = [ 77 | 'area', 78 | 'base', 79 | 'br', 80 | 'col', 81 | 'command', 82 | 'embed', 83 | 'hr', 84 | 'img', 85 | 'input', 86 | 'keygen', 87 | 'link', 88 | 'menuitem', 89 | 'meta', 90 | 'param', 91 | 'source', 92 | 'track', 93 | 'wbr', 94 | ]; 95 | 96 | const ATTRIBUTE_QUOTES_REQUIRED = /[\t\n\f\r "'`=<>]/; 97 | 98 | const defaultOptions = { 99 | closingSingleTag: undefined, 100 | quoteAllAttributes: true, 101 | replaceQuote: true, 102 | quoteStyle: quoteStyleEnum.Double, 103 | }; 104 | 105 | export function render(tree?: Node | Node[], options: Options = {}): string { 106 | let st = SINGLE_TAGS; 107 | 108 | if (options.singleTags) { 109 | st = [...new Set([...SINGLE_TAGS, ...options.singleTags])]; 110 | } 111 | 112 | options = { 113 | ...defaultOptions, 114 | ...options, 115 | singleTags: st, 116 | }; 117 | 118 | const { 119 | singleTags, 120 | closingSingleTag, 121 | quoteAllAttributes, 122 | replaceQuote, 123 | quoteStyle, 124 | } = options; 125 | 126 | const singleRegExp: RegExp[] = singleTags 127 | ?.filter((tag): tag is RegExp => tag instanceof RegExp) 128 | ?? []; 129 | 130 | if (!Array.isArray(tree)) { 131 | if (!tree) { 132 | tree = ''; 133 | } 134 | 135 | tree = [tree]; 136 | } 137 | 138 | return html(tree); 139 | 140 | function html(tree: Node[] | Node[][]) { 141 | let result = ''; 142 | 143 | for (const node of tree) { 144 | // Undefined, null, '', [], NaN 145 | if ( 146 | node === false 147 | || node === undefined 148 | || node === null 149 | || (typeof node === 'string' && node.length === 0) 150 | || Number.isNaN(node) 151 | ) { 152 | continue; 153 | } 154 | 155 | // Treat as new root tree if node is an array 156 | if (Array.isArray(node)) { 157 | if (node.length === 0) { 158 | continue; 159 | } 160 | 161 | result += html(node); 162 | 163 | continue; 164 | } 165 | 166 | if (typeof node === 'string' || typeof node === 'number') { 167 | result += node; 168 | 169 | continue; 170 | } 171 | 172 | if (!Array.isArray(node.content)) { 173 | if (!node.content) { 174 | node.content = ''; 175 | } 176 | 177 | node.content = [node.content]; 178 | } 179 | 180 | if (node.tag === false) { 181 | result += html(node.content); 182 | 183 | continue; 184 | } 185 | 186 | const tag = typeof node.tag === 'string' ? node.tag : 'div'; 187 | 188 | result += `<${tag}`; 189 | 190 | if (node.attrs) { 191 | result += attrs(node.attrs); 192 | } 193 | 194 | const closeAs = { 195 | [closingSingleTagTypeEnum.tag]: `>`, 196 | [closingSingleTagTypeEnum.slash]: ' />', 197 | [closingSingleTagTypeEnum.default]: '>', 198 | }; 199 | 200 | if (isSingleTag(tag)) { 201 | switch (closingSingleTag) { 202 | case closingSingleTagOptionEnum.tag: 203 | result += closeAs[closingSingleTagTypeEnum.tag]; 204 | 205 | break; 206 | case closingSingleTagOptionEnum.slash: 207 | result += closeAs[closingSingleTagTypeEnum.slash]; 208 | 209 | break; 210 | case closingSingleTagOptionEnum.closeAs: 211 | result += closeAs[node.closeAs 212 | ? closingSingleTagTypeEnum[node.closeAs] 213 | : closingSingleTagTypeEnum.default]; 214 | 215 | break; 216 | default: 217 | result += closeAs[closingSingleTagTypeEnum.default]; 218 | } 219 | 220 | if (node.content) { 221 | result += html(node.content); 222 | } 223 | } else if (closingSingleTag === closingSingleTagOptionEnum.closeAs && node.closeAs) { 224 | const type = node.closeAs 225 | ? closingSingleTagTypeEnum[node.closeAs] 226 | : closingSingleTagTypeEnum.default; 227 | result += `${closeAs[type]}${html(node.content)}`; 228 | } else { 229 | result += `>${html(node.content)}`; 230 | } 231 | } 232 | 233 | return result; 234 | } 235 | 236 | function isSingleTag(tag: string) { 237 | if (singleRegExp.length > 0) { 238 | return singleRegExp.some(reg => reg.test(tag)); 239 | } 240 | 241 | if (!singleTags?.includes(tag.toLowerCase())) { 242 | return false; 243 | } 244 | 245 | return true; 246 | } 247 | 248 | function attrs(object: Attributes) { 249 | let attr = ''; 250 | 251 | for (const [key, value] of Object.entries(object)) { 252 | if (typeof value === 'string') { 253 | if (isJSON(value)) { 254 | attr += makeAttr(key, value); 255 | } else if (quoteAllAttributes || ATTRIBUTE_QUOTES_REQUIRED.test(value)) { 256 | let attrValue = value; 257 | 258 | if (replaceQuote) { 259 | attrValue = value.replace(/"/g, '"'); 260 | } 261 | 262 | attr += makeAttr(key, attrValue, quoteStyle); 263 | } else if (value === '') { 264 | attr += ` ${key}`; 265 | } else { 266 | attr += ` ${key}=${value}`; 267 | } 268 | } else if (value === true) { 269 | attr += ` ${key}`; 270 | } else if (typeof value === 'number') { 271 | attr += makeAttr(key, value, quoteStyle); 272 | } 273 | } 274 | 275 | return attr; 276 | } 277 | 278 | function makeAttr(key: string, attrValue: string | number | boolean, quoteStyle = 1): string { 279 | if (quoteStyle === 1) { 280 | // Single Quote 281 | return ` ${key}='${attrValue}'`; 282 | } 283 | 284 | if (quoteStyle === 2) { 285 | // Double Quote 286 | return ` ${key}="${attrValue}"`; 287 | } 288 | 289 | // Smart Quote 290 | if (typeof attrValue === 'string' && attrValue.includes('"')) { 291 | return ` ${key}='${attrValue}'`; 292 | } 293 | 294 | return ` ${key}="${attrValue}"`; 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /test/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PostHTML Render 5 | 6 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/templates/parser.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | const tree = ['', '\n', { 3 | tag: 'html', 4 | attrs: { 5 | xmlns: 'http://www.w3.org/1999/xhtml', 6 | lang: 'en-US', 7 | }, 8 | content: ['\n', { 9 | tag: 'head', 10 | content: ['\n ', { 11 | tag: 'meta', 12 | attrs: { 13 | charset: 'utf-8', 14 | }, 15 | }, '\n ', { 16 | tag: 'title', 17 | content: ['Html'], 18 | }, '\n ', { 19 | tag: 'meta', 20 | attrs: { 21 | 'http-equiv': 'X-UA-Compatible', 22 | content: 'IE=EmulateIE7, IE=9', 23 | }, 24 | }, '\n ', { 25 | tag: 'meta', 26 | attrs: { 27 | name: 'viewport', 28 | content: 'width=device-width, initial-scale=1', 29 | }, 30 | }, '\n ', { 31 | tag: 'meta', 32 | attrs: { 33 | name: 'description', 34 | content: 'Description', 35 | }, 36 | }, '\n\n ', { 37 | tag: 'meta', 38 | attrs: { 39 | property: 'og:url', 40 | content: 'http://github.com/posthtml', 41 | }, 42 | }, '\n ', { 43 | tag: 'meta', 44 | attrs: { 45 | property: 'og:type', 46 | content: 'website', 47 | }, 48 | }, '\n ', { 49 | tag: 'meta', 50 | attrs: { 51 | property: 'og:site_name', 52 | content: 'PostHTML', 53 | }, 54 | }, '\n\n ', { 55 | tag: 'link', 56 | attrs: { 57 | rel: 'stylesheet', 58 | type: 'text/css', 59 | href: 'path/to/file.css', 60 | }, 61 | }, '\n ', { 62 | tag: 'script', 63 | attrs: { 64 | src: 'path/to/file.js', 65 | type: 'text/javascript', 66 | charset: 'utf-8', 67 | }, 68 | }, '\n\n ', { 69 | tag: 'script', 70 | content: ['\n console.log(\'PostHTML!\');\n '], 71 | }, '\n'], 72 | }, '\n', { 73 | tag: 'body', 74 | attrs: { 75 | onload: 'try{if(!google.j.b){document.f&&document.f.q.focus();document.gbqf&&document.gbqf.q.focus();}}catch(e){}if(document.images)new Image().src=\'/images/nav_logo231.png\'', 76 | }, 77 | content: ['\n\n ', { 78 | tag: 'h1', 79 | content: ['Title'], 80 | }, '\n ', { 81 | tag: 'p', 82 | content: ['Lorem ipsum dolor sit amet...'], 83 | }, '\n\n ', { 84 | tag: 'section', 85 | attrs: { 86 | class: 'foo', 87 | style: 'color: red;', 88 | }, 89 | content: ['\n ', { 90 | tag: 'header', 91 | attrs: { 92 | class: 'foo bar', 93 | style: 'color: blue; border: 1px solid', 94 | id: 'id', 95 | }, 96 | content: ['\n ', { 97 | tag: 'div', 98 | attrs: { 99 | class: 'foo bar baz', 100 | id: 'idd', 101 | 'data-url': 'url/to/', 102 | }, 103 | content: ['\n ', { 104 | tag: 'span', 105 | attrs: { 106 | id: 'idd', 107 | 'data-data': '{ foo: \'bar\' }', 108 | }, 109 | content: ['\n ', { 110 | tag: 'a', 111 | attrs: { 112 | href: '#', 113 | }, 114 | content: ['\n ', { 115 | tag: 'img', 116 | attrs: { 117 | src: 'path/to/img', 118 | }, 119 | }, '\n Link\n '], 120 | }, '\n '], 121 | }, '\n '], 122 | }, '\n '], 123 | }, '\n '], 124 | }, '\n\n ', { 125 | content: { 126 | tag: 'hr', 127 | }, 128 | }, '\n\n ', { 129 | tag: 'script', 130 | attrs: { 131 | type: 'text/javascript', 132 | }, 133 | content: [ 134 | '\n (function(){function k(a){++b;a=a||window.event;google.iTick(a.target||a.srcElement)}if(google.timers&&google.timers.load.t){var c,b,f;google.c.c.a&&(google.startTick("aft"),google.afte=!1);var g=document.getElementsByTagName("img");c=g.length;for(var d=b=0,a;d', 135 | ' 2 | 3 | 4 | 5 | Html 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 |

Title

24 |

Lorem ipsum dolor sit amet...

25 | 26 |
27 |
28 |
29 | 30 | 31 | 32 | Link 33 | 34 | 35 |
36 |
37 |
38 | 39 |

40 | 41 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /test/test-core.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import test from 'ava'; 4 | import parser from 'posthtml-parser'; 5 | import { 6 | closingSingleTagOptionEnum, 7 | closingSingleTagTypeEnum, 8 | quoteStyleEnum, 9 | render, 10 | } from '../src'; 11 | import tree from './templates/parser'; 12 | 13 | const html = fs.readFileSync(path.resolve(__dirname, 'templates/render.html'), 'utf8'); 14 | 15 | test('{String}', t => { 16 | const html = render('Hello world!'); 17 | const expected = 'Hello world!'; 18 | t.is(html, expected); 19 | }); 20 | 21 | test('{Number}', t => { 22 | const html = render(555); 23 | const expected = '555'; 24 | t.is(html, expected); 25 | }); 26 | 27 | test('{Array}', t => { 28 | const html = render(['Hello world!']); 29 | const expected = 'Hello world!'; 30 | t.is(html, expected); 31 | }); 32 | 33 | test('{Tags} {Empty}', t => { 34 | const html = render({content: ['Test']}); 35 | const expected = '
Test
'; 36 | t.is(html, expected); 37 | }); 38 | 39 | test('{Tags} {Boolean} (false) -> {String}', t => { 40 | t.is(render({tag: false, content: 'Test'}), 'Test'); 41 | t.is(render({tag: false, content: ['Test']}), 'Test'); 42 | }); 43 | 44 | test('{Tags} {Boolean} (false) -> {Number}', t => { 45 | t.is(render({tag: false, content: 555}), '555'); 46 | t.is(render({tag: false, content: [555]}), '555'); 47 | }); 48 | 49 | test('{Attrs} {Empty}', t => { 50 | const fixture = {attrs: {alt: ''}}; 51 | const expected = '
'; 52 | 53 | t.is(render(fixture), expected); 54 | }); 55 | 56 | test('{Attrs} {Single}', t => { 57 | const fixture = { 58 | attrs: { 59 | id: 'header', 60 | }, 61 | }; 62 | const expected = ''; 63 | 64 | t.is(render(fixture), expected); 65 | }); 66 | 67 | test('{Attrs} {Multiple}', t => { 68 | const fixture = { 69 | attrs: { 70 | id: 'header', 71 | style: 'color:red', 72 | 'data-id': 'header', 73 | }, 74 | }; 75 | const expected = ''; 76 | 77 | t.is(render(fixture), expected); 78 | }); 79 | 80 | test('{Attrs} {Boolean} (true)', t => { 81 | const fixture = { 82 | attrs: { 83 | disabled: true, 84 | }, 85 | }; 86 | const expected = '
'; 87 | 88 | t.is(render(fixture), expected); 89 | }); 90 | 91 | test('{Attrs} {Boolean} (false)', t => { 92 | const fixture = { 93 | attrs: { 94 | disabled: false, 95 | }, 96 | }; 97 | const expected = '
'; 98 | 99 | t.is(render(fixture), expected); 100 | }); 101 | 102 | test('{Attrs} {Number}', t => { 103 | const fixture = { 104 | attrs: { 105 | val: 5, 106 | }, 107 | }; 108 | const expected = '
'; 109 | 110 | t.is(render(fixture), expected); 111 | }); 112 | 113 | test('{Attrs} {String} (double quotes)', t => { 114 | const fixture = { 115 | attrs: { 116 | onclick: 'alert("hello world")', 117 | }, 118 | }; 119 | const expected = '
'; 120 | 121 | t.is(render(fixture), expected); 122 | }); 123 | 124 | test('{Attrs} {String} (json)', t => { 125 | const fixture = { 126 | attrs: { 127 | 'x-data': JSON.stringify({a: 1}), 128 | }, 129 | }; 130 | 131 | const expected = '
'; 132 | 133 | t.is(render(fixture), expected); 134 | }); 135 | 136 | test('{Content} {String}', t => { 137 | const fixture = {content: 'Hello world!'}; 138 | const expected = '
Hello world!
'; 139 | 140 | t.is(render(fixture), expected); 141 | }); 142 | 143 | test('{Content} {Array}', t => { 144 | const fixture = {content: ['Hello world!']}; 145 | const expected = '
Hello world!
'; 146 | 147 | t.is(render(fixture), expected); 148 | }); 149 | 150 | test('{Content} {Number}', t => { 151 | t.is(render({content: 555}), '
555
'); 152 | t.is(render({content: [555]}), '
555
'); 153 | }); 154 | 155 | test('{Content} {Array}', t => { 156 | t.is(render({content: [555]}), '
555
'); 157 | }); 158 | 159 | test('{Content} {Array}', t => { 160 | const fixture = { 161 | content: [ 162 | [ 163 | 555, 164 | {tag: 'div', content: 666}, 165 | 777, 166 | ], 167 | ], 168 | }; 169 | const expected = '
555
666
777
'; 170 | 171 | t.is(render(fixture), expected); 172 | }); 173 | 174 | test('{Content} {Array}', t => { 175 | const fixture = { 176 | content: [ 177 | [], 178 | [ 179 | {tag: 'style', content: 'body { color: red; }'}, 180 | ], 181 | ], 182 | }; 183 | const expected = '
'; 184 | 185 | t.is(render(fixture), expected); 186 | }); 187 | 188 | test('{Content} {Nested}', t => { 189 | const fixture = { 190 | content: [ 191 | { 192 | content: [ 193 | { 194 | content: ['Test', {}], 195 | }, 196 | ], 197 | }, 198 | ], 199 | }; 200 | const expected = '
Test
'; 201 | 202 | t.is(render(fixture), expected); 203 | }); 204 | 205 | test('{Tree} {Empty}', t => { 206 | t.is(render(), ''); 207 | }); 208 | 209 | test('{Tree} {String Template}', t => { 210 | const html = ` 211 |
String Template
212 | `; 213 | const tree = parser(html); 214 | t.is(html, render(tree)); 215 | }); 216 | 217 | test('{Tree} {HTML}', t => { 218 | t.is(html, render(tree)); 219 | }); 220 | 221 | test('{Tree} {Immutable}', t => { 222 | const tree = [{ 223 | tag: 'div', 224 | content: [ 225 | { 226 | tag: false, 227 | content: [ 228 | {tag: 'div'}, 229 | {tag: 'span', content: ['Text']}, 230 | ], 231 | }, 232 | ], 233 | }]; 234 | 235 | const html1 = JSON.stringify(tree); 236 | 237 | render(html1); 238 | 239 | const html2 = JSON.stringify(tree); 240 | 241 | t.is(html1, html2); 242 | }); 243 | 244 | test('{Tree} {With empty string}', t => { 245 | const tree = [ 246 | '', 247 | '', 248 | '', 249 | { 250 | tag: 'html', 251 | attrs: { 252 | lang: 'en', 253 | }, 254 | content: [ 255 | '', 256 | { 257 | tag: 'head', 258 | content: [ 259 | '', 260 | { 261 | tag: 'meta', 262 | attrs: { 263 | charset: 'utf-8', 264 | }, 265 | }, 266 | '', 267 | ], 268 | }, 269 | '', 270 | { 271 | tag: 'body', 272 | content: [ 273 | ' ', 274 | ], 275 | }, 276 | '', 277 | ], 278 | }, 279 | '', 280 | ]; 281 | const html = ' '; 282 | 283 | t.is(render(tree), html); 284 | }); 285 | 286 | test('{Tree} {With tag false}', t => { 287 | const tree = [ 288 | { 289 | tag: false, 290 | content: [], 291 | }, 292 | { 293 | tag: 'script', 294 | content: ['window.foo1 = \'foo\';', 'window.foo2 = \'foo\''], 295 | }, 296 | '\\n ', 297 | { 298 | tag: 'script', 299 | attrs: { 300 | src: './script-need-foo-variable.js', 301 | }, 302 | }, 303 | '\\n ', 304 | { 305 | tag: false, 306 | content: [], 307 | }, 308 | { 309 | tag: 'script', 310 | content: ['window.bar1 = \'foo\';', 'window.bar2 = \'bar\''], 311 | }, 312 | ]; 313 | const html = '\\n \\n '; 314 | 315 | t.is(render(tree), html); 316 | }); 317 | 318 | test('{Tree} {With ?}', t => { 319 | const tree = [ 320 | { 321 | tag: false, 322 | content: [ 323 | { 324 | tag: false, 325 | content: [ 326 | { 327 | tag: 'title', 328 | content: ['Title'], 329 | }, 330 | ], 331 | }, 332 | { 333 | tag: false, 334 | content: [ 335 | { 336 | tag: 'p', 337 | content: ['Hi'], 338 | }, 339 | ], 340 | }, 341 | ], 342 | }, 343 | ]; 344 | const html = 'Title

Hi

'; 345 | 346 | t.is(render(tree), html); 347 | }); 348 | 349 | test('{Options} {singleTag} Defaults', t => { 350 | const SINGLE_TAGS_LOWERCASE = [ 351 | 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr', 352 | ]; 353 | 354 | const SINGLE_TAGS_UPPERCASE = [ 355 | 'IMG', 356 | ]; 357 | 358 | const SINGLE_TAGS = SINGLE_TAGS_LOWERCASE.concat(SINGLE_TAGS_UPPERCASE); 359 | 360 | t.is( 361 | render(SINGLE_TAGS.map(tag => ({tag}))), 362 | SINGLE_TAGS.map(tag => `<${tag}>`).join(''), 363 | ); 364 | }); 365 | 366 | test('{Options} {singleTag} Custom {String}', t => { 367 | const options = {singleTags: ['rect']}; 368 | 369 | const fixture = {tag: 'rect'}; 370 | const expected = ''; 371 | 372 | t.is(render(fixture, options), expected); 373 | }); 374 | 375 | test('{Options} {singleTag} Custom {RegExp}', t => { 376 | const options = {singleTags: [/^%.*%$/]}; 377 | 378 | const fixture = {tag: '%=title%'}; 379 | const expected = '<%=title%>'; 380 | 381 | t.is(render(fixture, options), expected); 382 | }); 383 | 384 | test('{Options} {singleTag} Attrs', t => { 385 | const options = {singleTags: ['rect']}; 386 | 387 | const fixture = {tag: 'rect', attrs: {id: 'id'}}; 388 | const expected = ''; 389 | 390 | t.is(render(fixture, options), expected); 391 | }); 392 | 393 | test('{Options} {closingSingleTag} Tag', t => { 394 | const options = {closingSingleTag: closingSingleTagOptionEnum.tag}; 395 | 396 | const fixture = {tag: 'br'}; 397 | const expected = '

'; 398 | 399 | t.is(render(fixture, options), expected); 400 | }); 401 | 402 | test('{Options} {closingSingleTag} Slash', t => { 403 | const options = {closingSingleTag: closingSingleTagOptionEnum.slash}; 404 | 405 | const fixture = {tag: 'br'}; 406 | const expected = '
'; 407 | 408 | t.is(render(fixture, options), expected); 409 | }); 410 | 411 | test('{Options} {closingSingleTag} Slash with content', t => { 412 | const options = {closingSingleTag: closingSingleTagOptionEnum.slash}; 413 | 414 | const fixture = {tag: 'br', content: ['test']}; 415 | const expected = '
test'; 416 | 417 | t.is(render(fixture, options), expected); 418 | }); 419 | 420 | test('{Options} {closingSingleTag} Default', t => { 421 | const fixture = {tag: 'br'}; 422 | const expected = '
'; 423 | 424 | t.is(render(fixture), expected); 425 | }); 426 | 427 | test('{Options} {closingSingleTag} closeAs', t => { 428 | const options = {closingSingleTag: closingSingleTagOptionEnum.closeAs}; 429 | const fixture = { 430 | content: [ 431 | { 432 | content: [ 433 | { 434 | content: ['Test', {}], 435 | }, 436 | ], 437 | }, 438 | ], 439 | closeAs: closingSingleTagTypeEnum.default, 440 | }; 441 | const expected = '
Test
'; 442 | 443 | t.is(render(fixture, options), expected); 444 | }); 445 | 446 | test('{Options} {quoteAllAttributes} True', t => { 447 | const options = {quoteAllAttributes: true}; 448 | 449 | const fixture = {tag: 'a', attrs: {href: '/about/me/'}}; 450 | const expected = ''; 451 | 452 | t.is(render(fixture, options), expected); 453 | }); 454 | 455 | test('{Options} {quoteAllAttributes} False', t => { 456 | const options = {quoteAllAttributes: false}; 457 | 458 | const fixture = {tag: 'a', attrs: {href: '/about/me/'}}; 459 | const expected = ''; 460 | 461 | t.is(render(fixture, options), expected); 462 | }); 463 | 464 | test('{Options} {quoteAllAttributes} Required Space', t => { 465 | const options = {quoteAllAttributes: false}; 466 | 467 | const fixture = {tag: 'p', attrs: {id: 'asd adsasd'}}; 468 | const expected = '

'; 469 | 470 | t.is(render(fixture, options), expected); 471 | }); 472 | 473 | test('{Options} {quoteAllAttributes} Required Tab', t => { 474 | const options = {quoteAllAttributes: false}; 475 | 476 | const fixture = {tag: 'a', attrs: {href: '/about-\t-characters'}}; 477 | const expected = ''; 478 | 479 | t.is(render(fixture, options), expected); 480 | }); 481 | 482 | test('{Options} {quoteAllAttributes} Required Empty', t => { 483 | const options = {quoteAllAttributes: false}; 484 | 485 | const fixture = {tag: 'script', attrs: {async: ''}}; 486 | const expected = ''; 487 | 488 | t.is(render(fixture, options), expected); 489 | }); 490 | 491 | test('{Options} {quoteAllAttributes} Closing slash', t => { 492 | const options = { 493 | closingSingleTag: closingSingleTagOptionEnum.slash, 494 | quoteAllAttributes: false, 495 | }; 496 | 497 | // Note that is incorrect as that is parsed as 498 | // . 499 | 500 | const fixture = {tag: 'area', attrs: {href: 'foobar'}}; 501 | const expected = ''; 502 | 503 | t.is(render(fixture, options), expected); 504 | }); 505 | 506 | test('{Options} {replaceQuote} replace quote', t => { 507 | const options = {replaceQuote: false}; 508 | 509 | const fixture = {tag: 'img', attrs: {src: ''}}; 510 | const expected = '">'; 511 | t.is(render(fixture, options), expected); 512 | }); 513 | 514 | test('{Options} {replaceQuote} replace quote ternary operator', t => { 515 | const options = {replaceQuote: false}; 516 | 517 | const fixture = {tag: 'img', attrs: {src: ''}}; 518 | const expected = '">'; 519 | t.is(render(fixture, options), expected); 520 | }); 521 | 522 | test('{Options} {quoteStyle} 1 - single quote', t => { 523 | const options = {replaceQuote: false, quoteStyle: quoteStyleEnum.Single}; 524 | 525 | const fixture = {tag: 'img', attrs: {src: 'https://example.com/example.png', onload: 'testFunc("test")'}}; 526 | const expected = ''; 527 | 528 | t.is(render(fixture, options), expected); 529 | }); 530 | 531 | test('{Options} {quoteStyle} 2 - double quote', t => { 532 | const options = {replaceQuote: false, quoteStyle: quoteStyleEnum.Double}; 533 | 534 | const fixture = {tag: 'img', attrs: {src: 'https://example.com/example.png', onload: 'testFunc("test")'}}; 535 | const expected = ''; 536 | 537 | t.is(render(fixture, options), expected); 538 | }); 539 | 540 | test('{Options} {quoteStyle} 0 - smart quote', t => { 541 | const options = {replaceQuote: false, quoteStyle: quoteStyleEnum.Smart}; 542 | 543 | const fixture = {tag: 'img', attrs: {src: 'https://example.com/example.png', onload: 'testFunc("test")'}}; 544 | const expected = ''; 545 | 546 | t.is(render(fixture, options), expected); 547 | }); 548 | 549 | test('{QuoteStyle} for width/height attrs in img', t => { 550 | const fixture = { 551 | tag: 'img', 552 | attrs: { 553 | src: 'https://example.com/example.png', 554 | width: '20', 555 | height: '20', 556 | }, 557 | }; 558 | const expected = ''; 559 | 560 | t.is(render(fixture), expected); 561 | }); 562 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext"], 5 | "esModuleInterop": true, 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "strictNullChecks": true, 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /xo.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | space: true, 3 | ignores: ['test/render.test.js'], 4 | rules: { 5 | 'ava/no-import-test-files': 'off', 6 | 'unicorn/prefer-node-protocol': 'off', 7 | '@typescript-eslint/no-unsafe-call': 'off', 8 | '@typescript-eslint/restrict-template-expressions': 'off', 9 | 'import/extensions': 'off', 10 | 'ava/no-skip-test': 'off', 11 | 'ava/no-only-test': 'off', 12 | 'unicorn/prefer-module': 'off', 13 | }, 14 | }; 15 | --------------------------------------------------------------------------------