From ec7233ef6def1e1ffb547f578ba28d3409c1aa06 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Fri, 17 Nov 2023 19:12:19 +0700 Subject: [PATCH] Require Node.js 18 and add TypeScript types --- .editorconfig | 12 +++ .gitattributes | 1 + .github/security.md | 3 + .github/workflows/main.yml | 20 +++++ .github/workflows/tests.yml | 14 ---- .gitignore | 3 +- .gitmodules | 3 - .npmrc | 1 + History.md | 97 --------------------- LICENSE - MIT | 20 ----- Makefile | 14 ---- Readme.md | 34 -------- component.json | 19 ----- index.js | 163 +++++++++++++++--------------------- license | 10 +++ package.json | 79 +++++++++-------- readme.md | 28 +++++++ test.js | 128 ++++++++++++++++++++++++++++ test/domify.js | 130 ---------------------------- 19 files changed, 317 insertions(+), 462 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/security.md create mode 100644 .github/workflows/main.yml delete mode 100644 .github/workflows/tests.yml delete mode 100644 .gitmodules create mode 100644 .npmrc delete mode 100644 History.md delete mode 100644 LICENSE - MIT delete mode 100644 Makefile delete mode 100644 Readme.md delete mode 100644 component.json create mode 100644 license create mode 100644 readme.md create mode 100644 test.js delete mode 100644 test/domify.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1c6314a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = tab +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.yml] +indent_style = space +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/security.md b/.github/security.md new file mode 100644 index 0000000..5358dc5 --- /dev/null +++ b/.github/security.md @@ -0,0 +1,3 @@ +# Security Policy + +To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..5f816e2 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,20 @@ +name: CI +on: + - push + - pull_request +jobs: + test: + name: Node.js ${{ matrix.node-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: + - 16 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm test diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 03321b0..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Test - -on: [push] - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 - with: - node-version: 14 - - run: npm install - - run: npm test diff --git a/.gitignore b/.gitignore index d135df6..239ecff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ node_modules -components -build \ No newline at end of file +yarn.lock diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 0cc9de9..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "test/mocha"] - path = test/mocha - url = git://github.com/visionmedia/mocha.git diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/History.md b/History.md deleted file mode 100644 index f9e8938..0000000 --- a/History.md +++ /dev/null @@ -1,97 +0,0 @@ - -1.4.1 / 2021-04-27 -================== - - * Fix XSS via prototype pollution (#48) - * Setup CI (#49) - * add MIT license file - -1.4.0 / 2015-07-15 -================== - - * package: Add "license" attribute (#41, @pdehaan) - * index: Allow other implementations of `document` (#39, @tomekwi) - -1.3.3 / 2015-04-05 -================== - - * update readme - -1.3.2 / 2015-03-10 -================== - - * add support for `` svg elements - -1.3.1 / 2014-07-16 -================== - - * add script tag support for IE - -1.3.0 / 2014-07-14 -================== - - * index: allow an explicit `document` object to be passed in - * index: update JSDoc comments - * package: rename back to "domify" - -1.2.2 / 2014-02-10 -================== - - * package: rename to "component-domify" - * package: update "main" and "component" fields - * package: use 2 space tabs - * component: remove redundant "scripts" array entry - -1.2.1 / 2014-01-30 -================== - - * fix leading/trailing whitespace in text nodes - -1.2.0 / 2014-01-05 -================== - - * add basic SVG element support [jkroso] - -1.1.1 / 2013-11-05 -================== - - * use createTextNode() [jkroso] - -1.1.0 / 2013-11-05 -================== - - * do not rely on live .children NodeList [timoxley] - * generate text node if not given an html tag [timoxley] - -1.0.0 / 2013-06-13 -================== - - * return document fragments for multiple top level nodes - -0.2.0 / 2013-05-21 -================== - - * change to return an array at all times. Closes #7 - -0.1.0 / 2012-10-24 -================== - - * add support for body elements with classes [timoxley] - * add removal of dummy parent div [timoxley] - * fix tests - * fix Makefile - -0.0.3 / 2012-08-28 -================== - - * fix package.json - -0.0.2 / 2012-08-01 -================== - - * add support for tags. Closes #1 [domenic] - -0.0.1 / 2010-01-03 -================== - - * Initial release diff --git a/LICENSE - MIT b/LICENSE - MIT deleted file mode 100644 index 2106451..0000000 --- a/LICENSE - MIT +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2016 http://component.github.io/ - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile deleted file mode 100644 index dbddf7c..0000000 --- a/Makefile +++ /dev/null @@ -1,14 +0,0 @@ - -build: index.js components - @component build --dev - -components: - @component install --dev - -clean: - rm -fr build components - -test: - @mocha-phantomjs test/index.html - -.PHONY: build clean test diff --git a/Readme.md b/Readme.md deleted file mode 100644 index af3706c..0000000 --- a/Readme.md +++ /dev/null @@ -1,34 +0,0 @@ -# domify - -Turn HTML into DOM elements x-browser. - -## Usage - -Works out of the box in the browser: - -```js -var domify = require('domify'); - -document.addEventListener('DOMContentLoaded', function() { - var el = domify('

Hello there

'); - document.body.appendChild(el); -}); -``` - -You can also run it in *node* and *iojs*. Just pass a custom implementation of `document`: - -```js -var jsdom = require('jsdom').jsdom(); - -domify('

Hello there

', jsdom.defaultView.document); -``` - -## Running tests - -``` -$ npm test -``` - -## License - -MIT diff --git a/component.json b/component.json deleted file mode 100644 index a1c6eb8..0000000 --- a/component.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "domify", - "version": "1.4.1", - "description": "turn HTML into DOM elements", - "development": { - "component/assert": "*" - }, - "keywords": [ - "dom", - "html", - "client", - "browser", - "component" - ], - "scripts": [ - "index.js" - ], - "license": "MIT" -} diff --git a/index.js b/index.js index 7f13d11..78404be 100644 --- a/index.js +++ b/index.js @@ -1,112 +1,85 @@ - -/** - * Expose `parse`. - */ - -module.exports = parse; - -/** - * Tests for browser support. - */ - -var innerHTMLBug = false; -var bugTestDiv; -if (typeof document !== 'undefined') { - bugTestDiv = document.createElement('div'); - // Setup - bugTestDiv.innerHTML = '
a'; - // Make sure that link elements get serialized correctly by innerHTML - // This requires a wrapper element in IE - innerHTMLBug = !bugTestDiv.getElementsByTagName('link').length; - bugTestDiv = undefined; -} - -/** - * Wrap map from jquery. - */ - -var map = { - legend: [1, '
', '
'], - tr: [2, '', '
'], - col: [2, '', '
'], - // for script/link/style tags to work in IE6-8, you have to wrap - // in a div with a non-whitespace character in front, ha! - _default: innerHTMLBug ? [1, 'X
', '
'] : [0, '', ''] +const wrapMap = { + legend: [1, '
', '
'], + tr: [2, '', '
'], + col: [2, '', '
'], + _default: [0, '', ''], }; -map.td = -map.th = [3, '', '
']; +wrapMap.td += wrapMap.th = [3, '', '
']; -map.option = -map.optgroup = [1, '']; +wrapMap.option += wrapMap.optgroup = [1, '']; -map.thead = -map.tbody = -map.colgroup = -map.caption = -map.tfoot = [1, '', '
']; +wrapMap.thead += wrapMap.tbody += wrapMap.colgroup += wrapMap.caption += wrapMap.tfoot = [1, '', '
']; -map.polyline = -map.ellipse = -map.polygon = -map.circle = -map.text = -map.line = -map.path = -map.rect = -map.g = [1, '','']; +wrapMap.polyline += wrapMap.ellipse += wrapMap.polygon += wrapMap.circle += wrapMap.text += wrapMap.line += wrapMap.path += wrapMap.rect += wrapMap.g = [1, '', '']; /** * Parse `html` and return a DOM Node instance, which could be a TextNode, * HTML DOM Node of some kind (
for example), or a DocumentFragment * instance, depending on the contents of the `html` string. * - * @param {String} html - HTML string to "domify" + * @param {String} htmlString - HTML string to "domify" * @param {Document} doc - The `document` instance to create the Node for * @return {DOMNode} the TextNode, DOM Node, or DocumentFragment instance * @api private */ -function parse(html, doc) { - if ('string' != typeof html) throw new TypeError('String expected'); - - // default to the global `document` object - if (!doc) doc = document; - - // tag name - var m = /<([\w:]+)/.exec(html); - if (!m) return doc.createTextNode(html); - - html = html.replace(/^\s+|\s+$/g, ''); // Remove leading/trailing whitespace - - var tag = m[1]; - - // body support - if (tag == 'body') { - var el = doc.createElement('html'); - el.innerHTML = html; - return el.removeChild(el.lastChild); - } - - // wrap map - var wrap = Object.prototype.hasOwnProperty.call(map, tag) ? map[tag] : map._default; - var depth = wrap[0]; - var prefix = wrap[1]; - var suffix = wrap[2]; - var el = doc.createElement('div'); - el.innerHTML = prefix + html + suffix; - while (depth--) el = el.lastChild; - - // one element - if (el.firstChild == el.lastChild) { - return el.removeChild(el.firstChild); - } - - // several elements - var fragment = doc.createDocumentFragment(); - while (el.firstChild) { - fragment.appendChild(el.removeChild(el.firstChild)); - } - - return fragment; +function domify(htmlString, document = globalThis.document) { + if (typeof htmlString !== 'string') { + throw new TypeError('String expected'); + } + + const tagName = /<([\w:]+)/.exec(htmlString)?.[1]; + + if (!tagName) { + return document.createTextNode(htmlString); + } + + htmlString = htmlString.trim(); + + // Body support + if (tagName === 'body') { + const element = document.createElement('html'); + element.innerHTML = htmlString; + const {lastChild} = element; + lastChild.remove(); + return lastChild; + } + + // Wrap map + let [depth, prefix, suffix] = Object.hasOwn(wrapMap, tagName) ? wrapMap[tagName] : wrapMap._default; + let element = document.createElement('div'); + element.innerHTML = prefix + htmlString + suffix; + while (depth--) { + element = element.lastChild; + } + + // One element + if (element.firstChild === element.lastChild) { + const {firstChild} = element; + firstChild.remove(); + return firstChild; + } + + // Several elements + const fragment = document.createDocumentFragment(); + fragment.append(...element.childNodes); + + return fragment; } + +module.exports = domify; diff --git a/license b/license new file mode 100644 index 0000000..f9172e0 --- /dev/null +++ b/license @@ -0,0 +1,10 @@ +MIT License + +Copyright (c) TJ Holowaychuk +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/package.json b/package.json index 68b698d..f1a5614 100644 --- a/package.json +++ b/package.json @@ -1,36 +1,47 @@ { - "name": "domify", - "version": "1.4.2", - "description": "turn HTML into DOM elements", - "keywords": [ - "dom", - "html", - "client", - "browser", - "component" - ], - "author": "TJ Holowaychuk ", - "dependencies": {}, - "scripts": { - "test": "mocha -r jsdom-global/register" - }, - "component": { - "scripts": { - "domify/index.js": "index.js" - } - }, - "main": "index.js", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/sindresorhus/domify.git" - }, - "files": [ - "index.js" - ], - "devDependencies": { - "jsdom": "^16.5.3", - "jsdom-global": "^3.0.2", - "mocha": "^8.3.2" - } + "name": "domify", + "version": "1.4.2", + "description": "Turn a HTML string into DOM elements, cross-platform", + "license": "MIT", + "repository": "sindresorhus/domify", + "funding": "https://github.com/sponsors/sindresorhus", + "exports": { + "types": "./index.d.ts", + "default": "./index.js" + }, + "main": "./index.js", + "types": "./index.d.ts", + "sideEffects": false, + "engines": { + "node": ">=18" + }, + "scripts": { + "test": "xo && ava" + }, + "files": [ + "index.js", + "index.d.ts" + ], + "keywords": [ + "dom", + "html", + "client", + "browser", + "component", + "element", + "elements", + "string", + "document" + ], + "devDependencies": { + "ava": "^5.3.1", + "xo": "^0.56.0", + "jsdom": "^22.1.0" + }, + "xo": { + "rules": { + "no-multi-assign": "off", + "unicorn/prefer-module": "off" + } + } } diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..61562f8 --- /dev/null +++ b/readme.md @@ -0,0 +1,28 @@ +# domify + +> Turn a HTML string into DOM elements, cross-platform + +## Usage + +Works out of the box in the browser: + +```js +import domify from 'domify'; + +document.addEventListener('DOMContentLoaded', () => { + const element = domify('

Hello there

'); + document.body.appendChild(element); +}); +``` + +You can also run it in Node.js and other non-browser environments by passing a custom implementation of `document`: + +```js +import {JSDOM} from 'jsdom'; + +const jsdom = new JSDOM(); + +domify('

Hello there

', jsdom.window.document); +``` + +**Note:** For browser-only use, prefer [`DOMParser.parseFromString()`](https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString). diff --git a/test.js b/test.js new file mode 100644 index 0000000..ccc565a --- /dev/null +++ b/test.js @@ -0,0 +1,128 @@ +const test = require('ava'); +const {JSDOM} = require('jsdom'); +const baseDomify = require('./index.js'); + +const jsdom = new JSDOM(); +const domify = html => baseDomify(html, jsdom.window.document); + +test('converts HTML to DOM elements', t => { + const element = domify('

Hello

'); + t.is(element.nodeName, 'P'); + t.is(element.textContent, 'Hello'); + + const elements = domify('

one

two

three

'); + t.is(elements.textContent, 'onetwothree'); +}); + +test('ignores trailing/leading whitespace', t => { + const element = domify('

Hello

'); + t.is(element.nodeName, 'P'); + t.is(element.textContent, 'Hello'); +}); + +test('supports body tags', t => { + const element = domify(''); + t.is(element.nodeName, 'BODY'); +}); + +test('supports body tags with classes', t => { + const element = domify(''); + t.is(element.nodeName, 'BODY'); + t.is(element.className, 'page'); +}); + +test('supports legend tags', t => { + const element = domify('Hello'); + t.is(element.nodeName, 'LEGEND'); +}); + +test('supports table tags', t => { + const element = domify('
'); + t.is(element.nodeName, 'TABLE'); +}); + +test('supports thead tags', t => { + const element = domify(''); + t.is(element.nodeName, 'THEAD'); +}); + +test('supports tbody tags', t => { + const element = domify(''); + t.is(element.nodeName, 'TBODY'); +}); + +test('supports tfoot tags', t => { + const element = domify(''); + t.is(element.nodeName, 'TFOOT'); +}); + +test('supports caption tags', t => { + const element = domify(''); + t.is(element.nodeName, 'CAPTION'); +}); + +test('supports col tags', t => { + const element = domify(''); + t.is(element.nodeName, 'COL'); +}); + +test('supports td tags', t => { + const element = domify(''); + t.is(element.nodeName, 'TD'); +}); + +test('supports th tags', t => { + const element = domify(''); + t.is(element.nodeName, 'TH'); +}); + +test('supports tr tags', t => { + const element = domify(''); + t.is(element.nodeName, 'TR'); +}); + +test('supports script tags', t => { + const element = domify(''); + t.is(element.nodeName, 'SCRIPT'); +}); + +test('supports option tags', t => { + const element = domify(''); + t.is(element.nodeName, 'OPTION'); +}); + +test('supports optgroup tags', t => { + const element = domify(''); + t.is(element.nodeName, 'OPTGROUP'); +}); + +test('does not set parentElement', t => { + const element = domify('

Hello

'); + t.falsy(element.parentElement); + t.falsy(element.parentNode); +}); + +test('supports text', t => { + const element = domify('text goes here'); + t.is(element.nodeName, '#text'); +}); + +test('preserves trailing/leading spaces for textElement', t => { + const element = domify(' text goes here '); + t.is(element.nodeName, '#text'); + t.is(element.textContent, ' text goes here '); +}); + +if (globalThis.SVGElement) { + test('svg - supports path tag', t => { + t.true(domify('') instanceof globalThis.SVGPathElement); + }); + + test('svg - supports rect tag', t => { + t.true(domify('') instanceof globalThis.SVGRectElement); + }); + + test('svg - supports g tag', t => { + t.true(domify('') instanceof globalThis.SVGGElement); + }); +} diff --git a/test/domify.js b/test/domify.js deleted file mode 100644 index 59f52dc..0000000 --- a/test/domify.js +++ /dev/null @@ -1,130 +0,0 @@ - -var assert = require('assert'); -var domify = require('..'); - -describe('domify(html)', function(){ - it('should convert HTML to DOM elements', function(){ - var el = domify('

Hello

'); - assert('P' == el.nodeName); - assert('Hello' == el.textContent); - - var els = domify('

one

two

three

'); - assert('onetwothree' == els.textContent); - }) - - it('should ignore trailing/leading whitespace', function(){ - var el = domify('

Hello

'); - assert('P' == el.nodeName); - assert('Hello' == el.textContent); - }) - - it('should support body tags', function(){ - var el = domify(''); - assert('BODY' == el.nodeName); - }) - - it('should support body tags with classes', function(){ - var el = domify(''); - assert('BODY' == el.nodeName); - assert('page' == el.className); - }) - - it('should support legend tags', function(){ - var el = domify('Hello'); - assert('LEGEND' == el.nodeName); - }) - - it('should support table tags', function(){ - var el = domify('
'); - assert('TABLE' == el.nodeName); - }) - - it('should support thead tags', function(){ - var el = domify(''); - assert('THEAD' == el.nodeName); - }) - - it('should support tbody tags', function(){ - var el = domify(''); - assert('TBODY' == el.nodeName); - }) - - it('should support tfoot tags', function(){ - var el = domify(''); - assert('TFOOT' == el.nodeName); - }) - - it('should support caption tags', function(){ - var el = domify(''); - assert('CAPTION' == el.nodeName); - }) - - it('should support col tags', function(){ - var el = domify(''); - assert('COL' == el.nodeName); - }) - - it('should support td tags', function(){ - var el = domify(''); - assert('TD' == el.nodeName); - }) - - it('should support th tags', function(){ - var el = domify(''); - assert('TH' == el.nodeName); - }) - - it('should support tr tags', function(){ - var el = domify(''); - assert('TR' == el.nodeName); - }) - - it('should support script tags', function(){ - var el = domify(''); - assert('SCRIPT' == el.nodeName); - }) - - it('should support option tags', function(){ - var el = domify(''); - assert('OPTION' == el.nodeName); - }) - - it('should support optgroup tags', function(){ - var el = domify(''); - assert('OPTGROUP' == el.nodeName); - }) - - it('should not set parentElement', function() { - var el = domify('

Hello

'); - assert(!el.parentElement); - assert(!el.parentNode); - }) - - it('should support text', function(){ - var el = domify('text goes here'); - assert('#text' == el.nodeName); - }) - - it('should preserve trailing/leading spaces for textElement', function(){ - var el = domify(' text goes here '); - assert('#text' == el.nodeName); - assert(' text goes here ' == el.textContent); - }) - - // Only run SVG tests in environments that support them. - if (typeof SVGElement !== 'undefined'){ - describe('svg', function(){ - it('path', function(){ - assert(domify('') instanceof SVGPathElement); - }) - - it('rect', function(){ - assert(domify('') instanceof SVGRectElement); - }) - - it('g', function() { - assert(domify('') instanceof SVGGElement); - }) - }) - } -})