mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-09-14 00:36:32 +00:00
Auto-generate third-party license notices (#370)
Closes #294 Closes #371
This commit is contained in:
parent
208e4bbba3
commit
fc7d3aa457
8 changed files with 306 additions and 14 deletions
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
|
@ -46,21 +46,21 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
checks:
|
||||
- 'security advisories'
|
||||
- 'banned licenses and crates'
|
||||
- 'crate security advisories'
|
||||
- 'crate license compatibility'
|
||||
|
||||
# Prevent sudden announcement of a new advisory from failing ci:
|
||||
continue-on-error: ${{ matrix.checks == 'security advisories' }}
|
||||
continue-on-error: ${{ matrix.checks == 'crate security advisories' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||
if: matrix.checks == 'security advisories'
|
||||
if: matrix.checks == 'crate security advisories'
|
||||
with:
|
||||
command: check advisories
|
||||
|
||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||
if: matrix.checks == 'banned licenses and crates'
|
||||
if: matrix.checks == 'crate license compatibility'
|
||||
with:
|
||||
command: check bans licenses sources
|
||||
|
|
21
about.hbs
Normal file
21
about.hbs
Normal file
|
@ -0,0 +1,21 @@
|
|||
// Be careful to prevent auto-formatting from breaking this file's indentation
|
||||
// Replace this file with JSON output once this is resolved: https://github.com/EmbarkStudios/cargo-about/issues/73
|
||||
|
||||
module.exports = [
|
||||
{{#each licenses}}
|
||||
{
|
||||
licenseName: `{{name}}`,
|
||||
licenseText: `{{text}}`,
|
||||
packages: [
|
||||
{{#each used_by}}
|
||||
{
|
||||
name: `{{crate.name}}`,
|
||||
version: `{{crate.version}}`,
|
||||
author: `{{crate.authors}}`,
|
||||
repository: `{{crate.repository}}`,
|
||||
},
|
||||
{{/each}}
|
||||
],
|
||||
},
|
||||
{{/each}}
|
||||
];
|
6
about.toml
Normal file
6
about.toml
Normal file
|
@ -0,0 +1,6 @@
|
|||
accepted = [
|
||||
"Apache-2.0",
|
||||
"MIT",
|
||||
]
|
||||
ignore-build-dependencies = true
|
||||
ignore-dev-dependencies = true
|
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
node_modules/
|
||||
dist/
|
||||
wasm/pkg/
|
||||
rust-licenses.js
|
||||
|
|
134
frontend/package-lock.json
generated
134
frontend/package-lock.json
generated
|
@ -1408,6 +1408,12 @@
|
|||
"integrity": "sha1-bI4obRHtdoMn+OYuzuhzU8o+eLg=",
|
||||
"dev": true
|
||||
},
|
||||
"array-find-index": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
|
||||
"integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
|
||||
"dev": true
|
||||
},
|
||||
"array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
|
@ -6882,6 +6888,66 @@
|
|||
"type-check": "~0.3.2"
|
||||
}
|
||||
},
|
||||
"license-checker-webpack-plugin": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/license-checker-webpack-plugin/-/license-checker-webpack-plugin-0.2.1.tgz",
|
||||
"integrity": "sha512-rX8B+mH6fk1vxbnIu/UztqTEonQw95xwOkoRjX3TSrRZA/pbG9CWa3wnSo89KY/ej379JQoq050fsuthy6AU+A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"glob": "^7.1.6",
|
||||
"lodash.template": "^4.5.0",
|
||||
"minimatch": "^3.0.4",
|
||||
"semver": "^6.3.0",
|
||||
"spdx-expression-validate": "^2.0.0",
|
||||
"spdx-satisfies": "^5.0.0",
|
||||
"superstruct": "^0.10.12",
|
||||
"webpack-sources": "^1.4.3",
|
||||
"wrap-ansi": "^6.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"dev": true
|
||||
},
|
||||
"wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"lines-and-columns": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
|
||||
|
@ -6969,6 +7035,12 @@
|
|||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash._reinterpolate": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
|
||||
"integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
|
@ -6993,6 +7065,25 @@
|
|||
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.template": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz",
|
||||
"integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash._reinterpolate": "^3.0.0",
|
||||
"lodash.templatesettings": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"lodash.templatesettings": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz",
|
||||
"integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash._reinterpolate": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"lodash.transform": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.transform/-/lodash.transform-4.6.0.tgz",
|
||||
|
@ -10047,6 +10138,17 @@
|
|||
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
|
||||
"dev": true
|
||||
},
|
||||
"spdx-compare": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz",
|
||||
"integrity": "sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"array-find-index": "^1.0.2",
|
||||
"spdx-expression-parse": "^3.0.0",
|
||||
"spdx-ranges": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"spdx-correct": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
|
||||
|
@ -10073,12 +10175,38 @@
|
|||
"spdx-license-ids": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"spdx-expression-validate": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/spdx-expression-validate/-/spdx-expression-validate-2.0.0.tgz",
|
||||
"integrity": "sha512-b3wydZLM+Tc6CFvaRDBOF9d76oGIHNCLYFeHbftFXUWjnfZWganmDmvtM5sm1cRwJc/VDBMLyGGrsLFd1vOxbg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"spdx-expression-parse": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"spdx-license-ids": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz",
|
||||
"integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==",
|
||||
"dev": true
|
||||
},
|
||||
"spdx-ranges": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz",
|
||||
"integrity": "sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==",
|
||||
"dev": true
|
||||
},
|
||||
"spdx-satisfies": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-5.0.1.tgz",
|
||||
"integrity": "sha512-Nwor6W6gzFp8XX4neaKQ7ChV4wmpSh2sSDemMFSzHxpTw460jxFYeOn+jq4ybnSSw/5sc3pjka9MQPouksQNpw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"spdx-compare": "^1.0.0",
|
||||
"spdx-expression-parse": "^3.0.0",
|
||||
"spdx-ranges": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"spdy": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",
|
||||
|
@ -10369,6 +10497,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"superstruct": {
|
||||
"version": "0.10.13",
|
||||
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.10.13.tgz",
|
||||
"integrity": "sha512-W4SitSZ9MOyMPbHreoZVEneSZyPEeNGbdfJo/7FkJyRs/M3wQRFzq+t3S/NBwlrFSWdx1ONLjLb9pB+UKe4IqQ==",
|
||||
"dev": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
"description": "Graphite's web app frontend. Planned to be replaced by a native GUI written in Rust in the future.",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve || npm install && vue-cli-service serve",
|
||||
"build": "vue-cli-service build || npm install && vue-cli-service build",
|
||||
"serve": "vue-cli-service serve || (npm install && vue-cli-service serve)",
|
||||
"build": "cd .. && cargo install cargo-about && cargo about generate about.hbs > frontend/rust-licenses.js && cd frontend && (vue-cli-service build || (npm install && vue-cli-service build))",
|
||||
"lint": "vue-cli-service lint || (npm install && vue-cli-service lint)",
|
||||
"lint-no-fix": "vue-cli-service lint --no-fix || (echo 'Please run `npm run lint`. If the linter execution fails, try running `npm install` first.' && false)"
|
||||
"lint-no-fix": "vue-cli-service lint --no-fix || (echo 'There were lint errors. Please run `npm run lint` to fix auto-them. If the linter execution fails, try running `npm install` first.' && false)"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -34,6 +34,7 @@
|
|||
"eslint-plugin-import": "^2.24.2",
|
||||
"eslint-plugin-prettier-vue": "^3.1.0",
|
||||
"eslint-plugin-vue": "^7.17.0",
|
||||
"license-checker-webpack-plugin": "^0.2.1",
|
||||
"prettier": "^2.3.2",
|
||||
"sass": "^1.39.0",
|
||||
"sass-loader": "^8.0.2",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="menu-bar-input">
|
||||
<div class="entry-container">
|
||||
<div @click="handleLogoClick(entry)" class="entry">
|
||||
<div @click="() => window.open('https://www.graphite.design', '_blank')" class="entry">
|
||||
<IconLabel :icon="'GraphiteLogo'" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -152,7 +152,16 @@ const menuEntries: MenuListEntries = [
|
|||
{
|
||||
label: "Help",
|
||||
ref: undefined,
|
||||
children: [[{ label: "Menu entries coming soon" }]],
|
||||
children: [
|
||||
[
|
||||
{ label: "Report a Bug", action: () => window.open("https://github.com/GraphiteEditor/Graphite/issues/new", "_blank") },
|
||||
{ label: "Visit on GitHub", action: () => window.open("https://github.com/GraphiteEditor/Graphite", "_blank") },
|
||||
],
|
||||
[
|
||||
{ label: "Graphite License", action: () => window.open("https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/LICENSE.txt", "_blank") },
|
||||
{ label: "Third-Party Licenses", action: () => window.open("/third-party-licenses.txt", "_blank") },
|
||||
],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -165,9 +174,6 @@ export default defineComponent({
|
|||
if (menuEntry.ref) menuEntry.ref.setOpen();
|
||||
else throw new Error("The menu bar floating menu has no associated ref");
|
||||
},
|
||||
handleLogoClick() {
|
||||
window.open("https://www.graphite.design", "_blank");
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -1,6 +1,19 @@
|
|||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const path = require("path");
|
||||
const { unlink } = require("fs");
|
||||
|
||||
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
|
||||
const LicenseCheckerWebpackPlugin = require("license-checker-webpack-plugin");
|
||||
|
||||
let rustLicenses = [];
|
||||
let debugMode = false;
|
||||
try {
|
||||
// eslint-disable-next-line global-require, import/extensions, import/no-unresolved
|
||||
rustLicenses = require("./rust-licenses");
|
||||
} catch (_) {
|
||||
// Rust licenses are not generated by Cargo About except in release mode (`npm run build`)
|
||||
debugMode = true;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
lintOnSave: "warning",
|
||||
|
@ -18,7 +31,7 @@ module.exports = {
|
|||
(Plugin) =>
|
||||
new Plugin({
|
||||
crateDirectory: path.resolve(__dirname, "wasm"),
|
||||
// Remove when this issue is resolved https://github.com/wasm-tool/wasm-pack-plugin/issues/93
|
||||
// Remove when this issue is resolved: https://github.com/wasm-tool/wasm-pack-plugin/issues/93
|
||||
outDir: path.resolve(__dirname, "wasm/pkg"),
|
||||
watchDirectories: [
|
||||
path.resolve(__dirname, "../editor"),
|
||||
|
@ -30,6 +43,22 @@ module.exports = {
|
|||
)
|
||||
.end();
|
||||
|
||||
// License Checker Webpack Plugin validates the license compatibility of all dependencies which are compiled into the webpack bundle
|
||||
// It also writes the third-party license notices to a file which is displayed in the application
|
||||
// https://github.com/microsoft/license-checker-webpack-plugin
|
||||
config
|
||||
.plugin("license-checker")
|
||||
.use(LicenseCheckerWebpackPlugin)
|
||||
.init(
|
||||
(Plugin) =>
|
||||
new Plugin({
|
||||
allow: "(Apache-2.0 OR BSD-2-Clause OR BSD-3-Clause OR MIT)",
|
||||
emitError: true,
|
||||
outputFilename: "third-party-licenses.txt",
|
||||
outputWriter: formatThirdPartyLicenses,
|
||||
})
|
||||
);
|
||||
|
||||
// Vue SVG Loader enables importing .svg files into .vue single-file components and using them directly in the HTML
|
||||
// https://vue-svg-loader.js.org/
|
||||
config.module
|
||||
|
@ -47,3 +76,97 @@ module.exports = {
|
|||
.end();
|
||||
},
|
||||
};
|
||||
|
||||
function formatThirdPartyLicenses(jsLicenses) {
|
||||
// Remove the HTML character encoding caused by Handlebars
|
||||
const licenses = rustLicenses.map((rustLicense) => ({
|
||||
licenseName: htmlDecode(rustLicense.licenseName),
|
||||
licenseText: htmlDecode(rustLicense.licenseText),
|
||||
packages: rustLicense.packages.map((package) => ({
|
||||
name: htmlDecode(package.name),
|
||||
version: htmlDecode(package.version),
|
||||
author: htmlDecode(package.author).replace(/\[(.*), \]/, "$1"),
|
||||
repository: htmlDecode(package.repository),
|
||||
})),
|
||||
}));
|
||||
|
||||
// Augment the imported Rust license list with the provided JS license list
|
||||
jsLicenses.dependencies.forEach((jsLicense) => {
|
||||
const { name, version, author, repository, licenseName, licenseText } = jsLicense;
|
||||
|
||||
// Remove the `git+` or `git://` prefix and `.git` suffix
|
||||
const repo = repository ? repository.replace(/^.*(github.com\/.*?\/.*?)(?:.git)/, "https://$1") : repository;
|
||||
|
||||
const matchedLicense = licenses.find((license) => license.licenseName.trim() === licenseName.trim() && license.licenseText.trim() === licenseText.trim());
|
||||
|
||||
const packages = { name, version, author, repository: repo };
|
||||
if (matchedLicense) matchedLicense.packages.push(packages);
|
||||
else licenses.push({ licenseName, licenseText, packages: [packages] });
|
||||
});
|
||||
|
||||
// Sort the licenses, and the packages using each license, alphabetically
|
||||
licenses.sort((a, b) => a.licenseName.localeCompare(b.licenseName));
|
||||
licenses.forEach((license) => {
|
||||
license.packages.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
// Generate the formatted text file
|
||||
let formattedLicenseNotice = "THIRD-PARTY SOFTWARE LICENSE NOTICES\n\n";
|
||||
if (debugMode) formattedLicenseNotice += "WARNING: Licenses for Rust packages are excluded in debug mode to improve performance — do not release without their inclusion!\n\n";
|
||||
|
||||
licenses.forEach((license) => {
|
||||
let packagesWithSameLicense = "";
|
||||
license.packages.forEach((package) => {
|
||||
const { name, version, author, repository } = package;
|
||||
packagesWithSameLicense += `${name} ${version}${author ? ` - ${author}` : ""}${repository ? ` - ${repository}` : ""}\n`;
|
||||
});
|
||||
formattedLicenseNotice += `--------------------------------------------------------------------------------
|
||||
|
||||
The following packages are licensed under the terms of the ${license.licenseName} license:
|
||||
|
||||
${packagesWithSameLicense}
|
||||
|
||||
${license.licenseText}
|
||||
|
||||
`;
|
||||
});
|
||||
|
||||
// Clean up by deleting the `rust-licenses.js` Rust licenses data file generated by Cargo About
|
||||
unlink("./rust-licenses.js", (_) => _);
|
||||
|
||||
return formattedLicenseNotice;
|
||||
}
|
||||
|
||||
const htmlEntities = {
|
||||
nbsp: " ",
|
||||
copy: "©",
|
||||
reg: "®",
|
||||
lt: "<",
|
||||
gt: ">",
|
||||
amp: "&",
|
||||
apos: "'",
|
||||
// eslint-disable-next-line quotes
|
||||
quot: '"',
|
||||
};
|
||||
|
||||
function htmlDecode(str) {
|
||||
if (!str) return str;
|
||||
|
||||
return str.replace(/&([^;]+);/g, (entity, entityCode) => {
|
||||
let match;
|
||||
|
||||
if (entityCode in htmlEntities) {
|
||||
return htmlEntities[entityCode];
|
||||
}
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
if ((match = entityCode.match(/^#x([\da-fA-F]+)$/))) {
|
||||
return String.fromCharCode(parseInt(match[1], 16));
|
||||
}
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
if ((match = entityCode.match(/^#(\d+)$/))) {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return String.fromCharCode(~~match[1]);
|
||||
}
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue