custom table handler
parent
4db476881a
commit
a084fa23f5
124
index.js
124
index.js
|
@ -1,20 +1,18 @@
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
const turndown = require('turndown');
|
const turndown = require('turndown');
|
||||||
const turndownPluginGfm = require('turndown-plugin-gfm')
|
|
||||||
const { Readability } = require('@mozilla/readability');
|
const { Readability } = require('@mozilla/readability');
|
||||||
const JSDOM = require('jsdom').JSDOM;
|
const JSDOM = require('jsdom').JSDOM;
|
||||||
const common_filters = require('./url_to_markdown_common_filters');
|
const common_filters = require('./url_to_markdown_common_filters');
|
||||||
const validURL = require('@7c/validurl');
|
const validURL = require('@7c/validurl');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const htmlEntities = require('html-entities');
|
||||||
|
|
||||||
const port = process.env.PORT;
|
const port = process.env.PORT;
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
const service = new turndown();
|
const service = new turndown();
|
||||||
const tables = turndownPluginGfm.tables
|
|
||||||
service.use(tables)
|
|
||||||
|
|
||||||
const rateLimiter = rateLimit({
|
const rateLimiter = rateLimit({
|
||||||
windowMs: 30 * 1000,
|
windowMs: 30 * 1000,
|
||||||
|
@ -51,9 +49,9 @@ app.post('/', function(req, res) {
|
||||||
let markdown = process_dom(url, document, res);
|
let markdown = process_dom(url, document, res);
|
||||||
send_headers(res);
|
send_headers(res);
|
||||||
res.send(markdown);
|
res.send(markdown);
|
||||||
//} catch (error) {
|
/*} catch (error) {
|
||||||
// res.status(400).send("Could not parse that document");
|
res.status(400).send("Could not parse that document");
|
||||||
//}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -72,8 +70,13 @@ function process_dom(url, document, res) {
|
||||||
if (title)
|
if (title)
|
||||||
res.header("X-Title", encodeURIComponent(title.textContent));
|
res.header("X-Title", encodeURIComponent(title.textContent));
|
||||||
let reader = new Readability(document.window.document);
|
let reader = new Readability(document.window.document);
|
||||||
let readable = reader.parse();
|
let readable = reader.parse().content;
|
||||||
let markdown = service.turndown(readable.content);
|
let replacement = {placeholders:[], tables:[]}
|
||||||
|
readable = format_tables(readable, replacement);
|
||||||
|
let markdown = service.turndown(readable);
|
||||||
|
for (let i=0;i<replacement.placeholders.length;i++) {
|
||||||
|
markdown = markdown.replace(replacement.placeholders[i], replacement.tables[i]);
|
||||||
|
}
|
||||||
let result = (url) ? common_filters.filter(url, markdown) : markdown;
|
let result = (url) ? common_filters.filter(url, markdown) : markdown;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -86,3 +89,108 @@ function read_url(url, res) {
|
||||||
res.status(400).send("Sorry, could not fetch and convert that URL");
|
res.status(400).send("Sorry, could not fetch and convert that URL");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clean(str) {
|
||||||
|
str = str.replace(/<\/?[^>]+(>|$)/g, "");
|
||||||
|
str = str.replace(/(\r\n|\n|\r)/gm, "");
|
||||||
|
str = htmlEntities.decode(str);
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
function format_table(table) {
|
||||||
|
let result = "\n";
|
||||||
|
|
||||||
|
let caption = table.match(/<caption[^>]*>((?:.|\n)*)<\/caption>/i);
|
||||||
|
if (caption)
|
||||||
|
result += clean(caption[1]) + "\n\n";
|
||||||
|
|
||||||
|
let items = [];
|
||||||
|
|
||||||
|
// collect data
|
||||||
|
let rows = table.match(/(<tr[^>]*>(?:.|\n)*?<\/tr>)/gi);
|
||||||
|
let n_rows = rows.length;
|
||||||
|
for (let r=0;r<n_rows;r++) {
|
||||||
|
let item_cols = [];
|
||||||
|
let cols = rows[r].match(/<t[h|d][^>]*>(?:.|\n)*?<\/t[h|d]>/gi);
|
||||||
|
for (let c=0;c<cols.length;c++)
|
||||||
|
item_cols.push(clean(cols[c]));
|
||||||
|
items.push(item_cols);
|
||||||
|
}
|
||||||
|
|
||||||
|
// find number of columns
|
||||||
|
let n_cols=0;
|
||||||
|
for (let r=0;r<n_rows;r++) {
|
||||||
|
if (items[r].length > n_cols) {
|
||||||
|
n_cols = items[r].length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalise columns
|
||||||
|
for (let r=0;r<n_rows;r++) {
|
||||||
|
for (let c=0;c<n_cols;c++) {
|
||||||
|
if (typeof items[r][c] === 'undefined') {
|
||||||
|
items[r].push("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// correct widths
|
||||||
|
let column_widths = [];
|
||||||
|
for (let r=0;r<n_rows;r++) {
|
||||||
|
for (let c=0;c<n_cols;c++) {
|
||||||
|
column_widths.push(0);
|
||||||
|
}
|
||||||
|
for (let c=0;c<n_cols;c++) {
|
||||||
|
let l = items[r][c].length;
|
||||||
|
if (l>column_widths[c]) {
|
||||||
|
column_widths[c]=l;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let r=0;r<n_rows;r++) {
|
||||||
|
for (let c=0;c<n_cols;c++) {
|
||||||
|
items[r][c] = items[r][c].padEnd(column_widths[c], " ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// output table
|
||||||
|
if (n_rows >0 && n_cols > 0) {
|
||||||
|
if (n_rows > 1) {
|
||||||
|
result += "|";
|
||||||
|
for (let c=0;c<n_cols;c++) {
|
||||||
|
result += items[0][c];
|
||||||
|
result += "|";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result += "\n";
|
||||||
|
result += "|";
|
||||||
|
for (let c=0;c<n_cols;c++) {
|
||||||
|
result += "-".repeat(column_widths[c]) + "|";
|
||||||
|
}
|
||||||
|
result += "\n";
|
||||||
|
for (let r=1;r<n_rows;r++) {
|
||||||
|
result += "|";
|
||||||
|
for (let c=0;c<n_cols;c++) {
|
||||||
|
result += items[r][c];
|
||||||
|
result += "|";
|
||||||
|
}
|
||||||
|
result += "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function format_tables(html, replacements) {
|
||||||
|
const tables = html.match(/(<table[^>]*>(?:.|\n)*?<\/table>)/gi);
|
||||||
|
for (let t=0;t<tables.length;t++) {
|
||||||
|
let table = tables[t];
|
||||||
|
let markdown = format_table(table);
|
||||||
|
let placeholder = "urltomarkdowntableplaceholder"+t+Math.random();
|
||||||
|
replacements.placeholders[t] = placeholder;
|
||||||
|
replacements.tables[t] = markdown;
|
||||||
|
html = html.replace(table, "<p>"+placeholder+"</p>");
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -484,6 +484,11 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html-entities": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ=="
|
||||||
|
},
|
||||||
"node_modules/http-errors": {
|
"node_modules/http-errors": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
|
||||||
|
@ -990,11 +995,6 @@
|
||||||
"domino": "^2.1.6"
|
"domino": "^2.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/turndown-plugin-gfm": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/turndown-plugin-gfm/-/turndown-plugin-gfm-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg=="
|
|
||||||
},
|
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
|
||||||
|
|
|
@ -0,0 +1,205 @@
|
||||||
|
2.3.2
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Minimize data files, remove unnecessary files.
|
||||||
|
|
||||||
|
2.3.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Improve performance of `encode()`, `decode()` and `decodeEntity()` by using function inlining.
|
||||||
|
* Fix decoding HEX HTML entities in some cases.
|
||||||
|
|
||||||
|
2.3.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Add flow types.
|
||||||
|
|
||||||
|
2.2.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* A fast `decodeEntity()` method to decode a single HTML entity.
|
||||||
|
|
||||||
|
2.1.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Speed up both `encode()` and `decode()` methods.
|
||||||
|
|
||||||
|
2.1.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Add `extensive` mode to `encode()` method. This mode encodes all non-printable characters, non-ASCII characters and all characters with named references.
|
||||||
|
|
||||||
|
2.0.6
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Handle invalid numeric HTML entities: mimic browser behaviour.
|
||||||
|
|
||||||
|
2.0.5
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Handling behaviour of ambiguous ampersands.
|
||||||
|
|
||||||
|
2.0.4
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Fix webpack build warning.
|
||||||
|
|
||||||
|
2.0.3
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Handle invalid numeric HTML entities.
|
||||||
|
|
||||||
|
2.0.2
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Handle `null` and `undefined` text values.
|
||||||
|
|
||||||
|
2.0.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Fix decoding numeric HTML entities.
|
||||||
|
|
||||||
|
2.0.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Performance was greatly improved.
|
||||||
|
* New API: simpler and more flexible.
|
||||||
|
|
||||||
|
`htmlEntitiesInstance.encode(text)` -> `encode(text)`
|
||||||
|
|
||||||
|
Before:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {AllHtmlEntities} from 'html-entities';
|
||||||
|
|
||||||
|
const entities = new AllHtmlEntities();
|
||||||
|
console.log(
|
||||||
|
entities.encode('<Hello & World>')
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {encode} from 'html-entities';
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
encode('<Hello & World>')
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
`instance.encodeNonASCII(text)` -> `encode(text, {mode: 'nonAscii'})`
|
||||||
|
|
||||||
|
Before:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {AllHtmlEntities} from 'html-entities';
|
||||||
|
|
||||||
|
const entities = new AllHtmlEntities();
|
||||||
|
console.log(
|
||||||
|
entities.encodeNonASCII('& © ∆')
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {encode} from 'html-entities';
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
encode('& © ∆', {mode: 'nonAscii'})
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
`instance.encodeNonASCII(text)` -> `encode(text, {mode: 'nonAsciiPrintable'})`
|
||||||
|
|
||||||
|
Before:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {AllHtmlEntities} from 'html-entities';
|
||||||
|
|
||||||
|
const entities = new AllHtmlEntities();
|
||||||
|
console.log(
|
||||||
|
entities.encodeNonASCII('& © ∆ \x01')
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {encode} from 'html-entities';
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
encode('& © ∆ \x01', {mode: 'nonAsciiPrintable'})
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
`instance.decode(text)` -> `decode(text)`
|
||||||
|
|
||||||
|
Before:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {AllHtmlEntities} from 'html-entities';
|
||||||
|
|
||||||
|
const entities = new AllHtmlEntities();
|
||||||
|
console.log(
|
||||||
|
entities.decode('<>&')
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {decode} from 'html-entities';
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
decode('<>&')
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Different XML/HTML versions are now implemented via options instead of different classes.
|
||||||
|
|
||||||
|
Before:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {XmlEntities, Html4Entities, Html5Entities, AllHtmlEntities} from 'html-entities';
|
||||||
|
|
||||||
|
const xmlEntities = new XmlEntities();
|
||||||
|
const html4Entities = new Html4Entities();
|
||||||
|
const html5Entities = new Html5Entities();
|
||||||
|
const allHtmlEntities = new AllHtmlEntities();
|
||||||
|
|
||||||
|
console.log(xmlEntities.encode('<>&'));
|
||||||
|
console.log(html4Entities.encode('<>&©'));
|
||||||
|
console.log(html5Entities.encode('<>&©℞'));
|
||||||
|
console.log(allHtmlEntities.encode('<>&©℞'));
|
||||||
|
|
||||||
|
console.log(xmlEntities.decode('<>&'));
|
||||||
|
console.log(html4Entities.decode('<>&©'));
|
||||||
|
console.log(html5Entities.decode('<>&©℞'));
|
||||||
|
console.log(allHtmlEntities.decode('<>&©℞'));
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {encode, decode} from 'html-entities';
|
||||||
|
|
||||||
|
console.log(encode('<>&', {level: 'xml'}));
|
||||||
|
console.log(encode('<>&©', {level: 'html4', mode: 'nonAscii'}));
|
||||||
|
console.log(encode('<>&©℞', {level: 'html5', mode: 'nonAscii'}));
|
||||||
|
console.log(encode('<>&©℞', {level: 'all', mode: 'nonAscii'}));
|
||||||
|
|
||||||
|
console.log(decode('<>&', {level: 'xml'}));
|
||||||
|
console.log(decode('<>&©', {level: 'html4'}));
|
||||||
|
console.log(decode('<>&©℞', {level: 'html5'}));
|
||||||
|
console.log(decode('<>&©℞', {level: 'all'}));
|
||||||
|
```
|
|
@ -1,6 +1,4 @@
|
||||||
MIT License
|
Copyright (c) 2021 Dulin Marat
|
||||||
|
|
||||||
Copyright (c) 2017 Dom Christie
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
@ -9,13 +7,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
furnished to do so, subject to the following conditions:
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
The above copyright notice and this permission notice shall be included in
|
||||||
copies or substantial portions of the Software.
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
SOFTWARE.
|
THE SOFTWARE.
|
|
@ -0,0 +1,213 @@
|
||||||
|
html-entities
|
||||||
|
=============
|
||||||
|
|
||||||
|
Fastest HTML entities library.
|
||||||
|
|
||||||
|
Comes with both TypeScript and Flow types.
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install html-entities
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
### encode(text, options)
|
||||||
|
|
||||||
|
Encodes text replacing HTML special characters (`<>&"'`) plus other character ranges depending on `mode` option value.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {encode} from 'html-entities';
|
||||||
|
|
||||||
|
encode('< > " \' & © ∆');
|
||||||
|
// -> '< > " ' & © ∆'
|
||||||
|
|
||||||
|
encode('< ©', {mode: 'nonAsciiPrintable'});
|
||||||
|
// -> '< ©'
|
||||||
|
|
||||||
|
encode('< ©', {mode: 'nonAsciiPrintable', level: 'xml'});
|
||||||
|
// -> '< ©'
|
||||||
|
```
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
#### level
|
||||||
|
|
||||||
|
* `all` alias to `html5` (default).
|
||||||
|
* `html5` uses `HTML5` named references.
|
||||||
|
* `html4` uses `HTML4` named references.
|
||||||
|
* `xml` uses `XML` named references.
|
||||||
|
|
||||||
|
#### mode
|
||||||
|
|
||||||
|
* `specialChars` encodes only HTML special characters (default).
|
||||||
|
* `nonAscii` encodes HTML special characters and everything outside of the [ASCII character range](https://en.wikipedia.org/wiki/ASCII).
|
||||||
|
* `nonAsciiPrintable` encodes HTML special characters and everything outiside of the [ASCII printable characters](https://en.wikipedia.org/wiki/ASCII#Printable_characters).
|
||||||
|
* `extensive` encodes all non-printable characters, non-ASCII characters and all characters with named references.
|
||||||
|
|
||||||
|
#### numeric
|
||||||
|
|
||||||
|
* `decimal` uses decimal numbers when encoding html entities. i.e. `©` (default).
|
||||||
|
* `hexadecimal` uses hexadecimal numbers when encoding html entities. i.e. `©`.
|
||||||
|
|
||||||
|
|
||||||
|
### decode(text, options)
|
||||||
|
|
||||||
|
Decodes text replacing entities to characters. Unknown entities are left as is.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {decode} from 'html-entities';
|
||||||
|
|
||||||
|
decode('< > " ' & © ∆');
|
||||||
|
// -> '< > " \' & © ∆'
|
||||||
|
|
||||||
|
decode('©', {level: 'html5'});
|
||||||
|
// -> '©'
|
||||||
|
|
||||||
|
decode('©', {level: 'xml'});
|
||||||
|
// -> '©'
|
||||||
|
```
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
#### level
|
||||||
|
|
||||||
|
* `all` alias to `html5` (default).
|
||||||
|
* `html5` uses `HTML5` named references.
|
||||||
|
* `html4` uses `HTML4` named references.
|
||||||
|
* `xml` uses `XML` named references.
|
||||||
|
|
||||||
|
#### scope
|
||||||
|
|
||||||
|
* `body` emulates behavior of browser when parsing tag bodies: entities without semicolon are also replaced (default).
|
||||||
|
* `attribute` emulates behavior of browser when parsing tag attributes: entities without semicolon are replaced when not followed by equality sign `=`.
|
||||||
|
* `strict` ignores entities without semicolon.
|
||||||
|
|
||||||
|
### decodeEntity(text, options)
|
||||||
|
|
||||||
|
Decodes a single HTML entity. Unknown entitiy is left as is.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {decodeEntity} from 'html-entities';
|
||||||
|
|
||||||
|
decodeEntity('<');
|
||||||
|
// -> '<'
|
||||||
|
|
||||||
|
decodeEntity('©', {level: 'html5'});
|
||||||
|
// -> '©'
|
||||||
|
|
||||||
|
decodeEntity('©', {level: 'xml'});
|
||||||
|
// -> '©'
|
||||||
|
```
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
#### level
|
||||||
|
|
||||||
|
* `all` alias to `html5` (default).
|
||||||
|
* `html5` uses `HTML5` named references.
|
||||||
|
* `html4` uses `HTML4` named references.
|
||||||
|
* `xml` uses `XML` named references.
|
||||||
|
|
||||||
|
Performance
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Statistically significant comparison with other libraries using `benchmark.js`.
|
||||||
|
Results by this library are marked with `*`.
|
||||||
|
The source code of the benchmark is available at `benchmark/benchmark.ts`.
|
||||||
|
|
||||||
|
```
|
||||||
|
Common
|
||||||
|
|
||||||
|
Initialization / Load speed
|
||||||
|
|
||||||
|
* #1: html-entities x 2,544,400 ops/sec ±4.52% (77 runs sampled)
|
||||||
|
#2: entities x 1,757,526 ops/sec ±3.99% (81 runs sampled)
|
||||||
|
#3: he x 1,281,542 ops/sec ±9.31% (74 runs sampled)
|
||||||
|
|
||||||
|
HTML5
|
||||||
|
|
||||||
|
Encode test
|
||||||
|
|
||||||
|
* #1: html-entities.encode - html5, nonAscii x 402,711 ops/sec ±0.61% (92 runs sampled)
|
||||||
|
* #2: html-entities.encode - html5, nonAsciiPrintable x 402,631 ops/sec ±2.99% (92 runs sampled)
|
||||||
|
* #3: html-entities.encode - html5, extensive x 269,162 ops/sec ±0.26% (97 runs sampled)
|
||||||
|
#4: entities.encodeNonAsciiHTML x 260,447 ops/sec ±2.53% (95 runs sampled)
|
||||||
|
#5: entities.encodeHTML x 101,059 ops/sec ±3.99% (91 runs sampled)
|
||||||
|
#6: he.encode x 93,180 ops/sec ±3.17% (92 runs sampled)
|
||||||
|
|
||||||
|
Decode test
|
||||||
|
|
||||||
|
* #1: html-entities.decode - html5, attribute x 340,043 ops/sec ±2.82% (92 runs sampled)
|
||||||
|
* #2: html-entities.decode - html5, body x 330,002 ops/sec ±1.52% (87 runs sampled)
|
||||||
|
* #3: html-entities.decode - html5, strict x 320,582 ops/sec ±5.34% (88 runs sampled)
|
||||||
|
#4: entities.decodeHTMLStrict x 286,294 ops/sec ±3.14% (89 runs sampled)
|
||||||
|
#5: entities.decodeHTML x 232,856 ops/sec ±3.05% (90 runs sampled)
|
||||||
|
#6: he.decode x 163,300 ops/sec ±0.62% (92 runs sampled)
|
||||||
|
|
||||||
|
HTML4
|
||||||
|
|
||||||
|
Encode test
|
||||||
|
|
||||||
|
* #1: html-entities.encode - html4, nonAsciiPrintable x 391,885 ops/sec ±0.27% (95 runs sampled)
|
||||||
|
* #2: html-entities.encode - html4, nonAscii x 400,086 ops/sec ±2.54% (94 runs sampled)
|
||||||
|
* #3: html-entities.encode - html4, extensive x 193,623 ops/sec ±2.70% (92 runs sampled)
|
||||||
|
|
||||||
|
Decode test
|
||||||
|
|
||||||
|
* #1: html-entities.decode - html4, attribute x 356,174 ops/sec ±0.49% (96 runs sampled)
|
||||||
|
* #2: html-entities.decode - html4, body x 342,666 ops/sec ±2.38% (91 runs sampled)
|
||||||
|
* #3: html-entities.decode - html4, strict x 341,667 ops/sec ±4.46% (87 runs sampled)
|
||||||
|
|
||||||
|
XML
|
||||||
|
|
||||||
|
Encode test
|
||||||
|
|
||||||
|
* #1: html-entities.encode - xml, nonAscii x 450,968 ops/sec ±2.73% (92 runs sampled)
|
||||||
|
* #2: html-entities.encode - xml, nonAsciiPrintable x 432,058 ops/sec ±4.12% (93 runs sampled)
|
||||||
|
* #3: html-entities.encode - xml, extensive x 265,336 ops/sec ±3.41% (93 runs sampled)
|
||||||
|
#4: entities.encodeXML x 254,862 ops/sec ±3.01% (95 runs sampled)
|
||||||
|
|
||||||
|
Decode test
|
||||||
|
|
||||||
|
* #1: html-entities.decode - xml, strict x 432,820 ops/sec ±0.53% (89 runs sampled)
|
||||||
|
* #2: html-entities.decode - xml, attribute x 426,037 ops/sec ±0.75% (94 runs sampled)
|
||||||
|
* #3: html-entities.decode - xml, body x 424,618 ops/sec ±3.47% (93 runs sampled)
|
||||||
|
#4: entities.decodeXML x 378,536 ops/sec ±2.48% (93 runs sampled)
|
||||||
|
|
||||||
|
Escaping
|
||||||
|
|
||||||
|
Escape test
|
||||||
|
|
||||||
|
* #1: html-entities.encode - xml, specialChars x 1,424,362 ops/sec ±0.55% (95 runs sampled)
|
||||||
|
#2: he.escape x 962,420 ops/sec ±3.12% (94 runs sampled)
|
||||||
|
#3: entities.escapeUTF8 x 443,138 ops/sec ±1.06% (90 runs sampled)
|
||||||
|
#4: entities.escape x 197,515 ops/sec ±2.73% (91 runs sampled)
|
||||||
|
```
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
Security contact information
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
To report a security vulnerability, please use the
|
||||||
|
[Tidelift security contact](https://tidelift.com/security). Tidelift will
|
||||||
|
coordinate the fix and disclosure.
|
||||||
|
|
||||||
|
`html-entities` for enterprise
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
Available as part of the Tidelift Subscription
|
||||||
|
|
||||||
|
The maintainers of `html-entities` and thousands of other packages are working with
|
||||||
|
Tidelift to deliver commercial support and maintenance for the open source
|
||||||
|
dependencies you use to build your applications. Save time, reduce risk, and
|
||||||
|
improve code health, while paying the maintainers of the exact dependencies you
|
||||||
|
use.
|
||||||
|
[Learn more.](https://tidelift.com/subscription/pkg/npm-html-entities?utm_source=npm-html-entities&utm_medium=referral&utm_campaign=enterprise)
|
|
@ -0,0 +1,20 @@
|
||||||
|
export declare type Level = 'xml' | 'html4' | 'html5' | 'all';
|
||||||
|
interface CommonOptions {
|
||||||
|
level?: Level;
|
||||||
|
}
|
||||||
|
export declare type EncodeMode = 'specialChars' | 'nonAscii' | 'nonAsciiPrintable' | 'extensive';
|
||||||
|
export interface EncodeOptions extends CommonOptions {
|
||||||
|
mode?: EncodeMode;
|
||||||
|
numeric?: 'decimal' | 'hexadecimal';
|
||||||
|
}
|
||||||
|
export declare type DecodeScope = 'strict' | 'body' | 'attribute';
|
||||||
|
export interface DecodeOptions extends CommonOptions {
|
||||||
|
scope?: DecodeScope;
|
||||||
|
}
|
||||||
|
/** Encodes all the necessary (specified by `level`) characters in the text */
|
||||||
|
export declare function encode(text: string | undefined | null, { mode, numeric, level }?: EncodeOptions): string;
|
||||||
|
/** Decodes a single entity */
|
||||||
|
export declare function decodeEntity(entity: string | undefined | null, { level }?: CommonOptions): string;
|
||||||
|
/** Decodes all entities in the text */
|
||||||
|
export declare function decode(text: string | undefined | null, { level, scope }?: DecodeOptions): string;
|
||||||
|
export {};
|
|
@ -0,0 +1,198 @@
|
||||||
|
"use strict";
|
||||||
|
var __assign = (this && this.__assign) || function () {
|
||||||
|
__assign = Object.assign || function(t) {
|
||||||
|
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||||
|
s = arguments[i];
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||||
|
t[p] = s[p];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
return __assign.apply(this, arguments);
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
var named_references_1 = require("./named-references");
|
||||||
|
var numeric_unicode_map_1 = require("./numeric-unicode-map");
|
||||||
|
var surrogate_pairs_1 = require("./surrogate-pairs");
|
||||||
|
var allNamedReferences = __assign(__assign({}, named_references_1.namedReferences), { all: named_references_1.namedReferences.html5 });
|
||||||
|
var encodeRegExps = {
|
||||||
|
specialChars: /[<>'"&]/g,
|
||||||
|
nonAscii: /(?:[<>'"&\u0080-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])/g,
|
||||||
|
nonAsciiPrintable: /(?:[<>'"&\x01-\x08\x11-\x15\x17-\x1F\x7f-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])/g,
|
||||||
|
extensive: /(?:[\x01-\x0c\x0e-\x1f\x21-\x2c\x2e-\x2f\x3a-\x40\x5b-\x60\x7b-\x7d\x7f-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])/g
|
||||||
|
};
|
||||||
|
var defaultEncodeOptions = {
|
||||||
|
mode: 'specialChars',
|
||||||
|
level: 'all',
|
||||||
|
numeric: 'decimal'
|
||||||
|
};
|
||||||
|
/** Encodes all the necessary (specified by `level`) characters in the text */
|
||||||
|
function encode(text, _a) {
|
||||||
|
var _b = _a === void 0 ? defaultEncodeOptions : _a, _c = _b.mode, mode = _c === void 0 ? 'specialChars' : _c, _d = _b.numeric, numeric = _d === void 0 ? 'decimal' : _d, _e = _b.level, level = _e === void 0 ? 'all' : _e;
|
||||||
|
if (!text) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
var encodeRegExp = encodeRegExps[mode];
|
||||||
|
var references = allNamedReferences[level].characters;
|
||||||
|
var isHex = numeric === 'hexadecimal';
|
||||||
|
encodeRegExp.lastIndex = 0;
|
||||||
|
var _b = encodeRegExp.exec(text);
|
||||||
|
var _c;
|
||||||
|
if (_b) {
|
||||||
|
_c = '';
|
||||||
|
var _d = 0;
|
||||||
|
do {
|
||||||
|
if (_d !== _b.index) {
|
||||||
|
_c += text.substring(_d, _b.index);
|
||||||
|
}
|
||||||
|
var _e = _b[0];
|
||||||
|
var result_1 = references[_e];
|
||||||
|
if (!result_1) {
|
||||||
|
var code_1 = _e.length > 1 ? surrogate_pairs_1.getCodePoint(_e, 0) : _e.charCodeAt(0);
|
||||||
|
result_1 = (isHex ? '&#x' + code_1.toString(16) : '&#' + code_1) + ';';
|
||||||
|
}
|
||||||
|
_c += result_1;
|
||||||
|
_d = _b.index + _e.length;
|
||||||
|
} while ((_b = encodeRegExp.exec(text)));
|
||||||
|
if (_d !== text.length) {
|
||||||
|
_c += text.substring(_d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_c =
|
||||||
|
text;
|
||||||
|
}
|
||||||
|
return _c;
|
||||||
|
}
|
||||||
|
exports.encode = encode;
|
||||||
|
var defaultDecodeOptions = {
|
||||||
|
scope: 'body',
|
||||||
|
level: 'all'
|
||||||
|
};
|
||||||
|
var strict = /&(?:#\d+|#[xX][\da-fA-F]+|[0-9a-zA-Z]+);/g;
|
||||||
|
var attribute = /&(?:#\d+|#[xX][\da-fA-F]+|[0-9a-zA-Z]+)[;=]?/g;
|
||||||
|
var baseDecodeRegExps = {
|
||||||
|
xml: {
|
||||||
|
strict: strict,
|
||||||
|
attribute: attribute,
|
||||||
|
body: named_references_1.bodyRegExps.xml
|
||||||
|
},
|
||||||
|
html4: {
|
||||||
|
strict: strict,
|
||||||
|
attribute: attribute,
|
||||||
|
body: named_references_1.bodyRegExps.html4
|
||||||
|
},
|
||||||
|
html5: {
|
||||||
|
strict: strict,
|
||||||
|
attribute: attribute,
|
||||||
|
body: named_references_1.bodyRegExps.html5
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var decodeRegExps = __assign(__assign({}, baseDecodeRegExps), { all: baseDecodeRegExps.html5 });
|
||||||
|
var fromCharCode = String.fromCharCode;
|
||||||
|
var outOfBoundsChar = fromCharCode(65533);
|
||||||
|
var defaultDecodeEntityOptions = {
|
||||||
|
level: 'all'
|
||||||
|
};
|
||||||
|
/** Decodes a single entity */
|
||||||
|
function decodeEntity(entity, _a) {
|
||||||
|
var _b = (_a === void 0 ? defaultDecodeEntityOptions : _a).level, level = _b === void 0 ? 'all' : _b;
|
||||||
|
if (!entity) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
var _b = entity;
|
||||||
|
var decodeEntityLastChar_1 = entity[entity.length - 1];
|
||||||
|
if (false
|
||||||
|
&& decodeEntityLastChar_1 === '=') {
|
||||||
|
_b =
|
||||||
|
entity;
|
||||||
|
}
|
||||||
|
else if (false
|
||||||
|
&& decodeEntityLastChar_1 !== ';') {
|
||||||
|
_b =
|
||||||
|
entity;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var decodeResultByReference_1 = allNamedReferences[level].entities[entity];
|
||||||
|
if (decodeResultByReference_1) {
|
||||||
|
_b = decodeResultByReference_1;
|
||||||
|
}
|
||||||
|
else if (entity[0] === '&' && entity[1] === '#') {
|
||||||
|
var decodeSecondChar_1 = entity[2];
|
||||||
|
var decodeCode_1 = decodeSecondChar_1 == 'x' || decodeSecondChar_1 == 'X'
|
||||||
|
? parseInt(entity.substr(3), 16)
|
||||||
|
: parseInt(entity.substr(2));
|
||||||
|
_b =
|
||||||
|
decodeCode_1 >= 0x10ffff
|
||||||
|
? outOfBoundsChar
|
||||||
|
: decodeCode_1 > 65535
|
||||||
|
? surrogate_pairs_1.fromCodePoint(decodeCode_1)
|
||||||
|
: fromCharCode(numeric_unicode_map_1.numericUnicodeMap[decodeCode_1] || decodeCode_1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _b;
|
||||||
|
}
|
||||||
|
exports.decodeEntity = decodeEntity;
|
||||||
|
/** Decodes all entities in the text */
|
||||||
|
function decode(text, _a) {
|
||||||
|
var decodeSecondChar_1 = _a === void 0 ? defaultDecodeOptions : _a, decodeCode_1 = decodeSecondChar_1.level, level = decodeCode_1 === void 0 ? 'all' : decodeCode_1, _b = decodeSecondChar_1.scope, scope = _b === void 0 ? level === 'xml' ? 'strict' : 'body' : _b;
|
||||||
|
if (!text) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
var decodeRegExp = decodeRegExps[level][scope];
|
||||||
|
var references = allNamedReferences[level].entities;
|
||||||
|
var isAttribute = scope === 'attribute';
|
||||||
|
var isStrict = scope === 'strict';
|
||||||
|
decodeRegExp.lastIndex = 0;
|
||||||
|
var replaceMatch_1 = decodeRegExp.exec(text);
|
||||||
|
var replaceResult_1;
|
||||||
|
if (replaceMatch_1) {
|
||||||
|
replaceResult_1 = '';
|
||||||
|
var replaceLastIndex_1 = 0;
|
||||||
|
do {
|
||||||
|
if (replaceLastIndex_1 !== replaceMatch_1.index) {
|
||||||
|
replaceResult_1 += text.substring(replaceLastIndex_1, replaceMatch_1.index);
|
||||||
|
}
|
||||||
|
var replaceInput_1 = replaceMatch_1[0];
|
||||||
|
var decodeResult_1 = replaceInput_1;
|
||||||
|
var decodeEntityLastChar_2 = replaceInput_1[replaceInput_1.length - 1];
|
||||||
|
if (isAttribute
|
||||||
|
&& decodeEntityLastChar_2 === '=') {
|
||||||
|
decodeResult_1 = replaceInput_1;
|
||||||
|
}
|
||||||
|
else if (isStrict
|
||||||
|
&& decodeEntityLastChar_2 !== ';') {
|
||||||
|
decodeResult_1 = replaceInput_1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var decodeResultByReference_2 = references[replaceInput_1];
|
||||||
|
if (decodeResultByReference_2) {
|
||||||
|
decodeResult_1 = decodeResultByReference_2;
|
||||||
|
}
|
||||||
|
else if (replaceInput_1[0] === '&' && replaceInput_1[1] === '#') {
|
||||||
|
var decodeSecondChar_2 = replaceInput_1[2];
|
||||||
|
var decodeCode_2 = decodeSecondChar_2 == 'x' || decodeSecondChar_2 == 'X'
|
||||||
|
? parseInt(replaceInput_1.substr(3), 16)
|
||||||
|
: parseInt(replaceInput_1.substr(2));
|
||||||
|
decodeResult_1 =
|
||||||
|
decodeCode_2 >= 0x10ffff
|
||||||
|
? outOfBoundsChar
|
||||||
|
: decodeCode_2 > 65535
|
||||||
|
? surrogate_pairs_1.fromCodePoint(decodeCode_2)
|
||||||
|
: fromCharCode(numeric_unicode_map_1.numericUnicodeMap[decodeCode_2] || decodeCode_2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
replaceResult_1 += decodeResult_1;
|
||||||
|
replaceLastIndex_1 = replaceMatch_1.index + replaceInput_1.length;
|
||||||
|
} while ((replaceMatch_1 = decodeRegExp.exec(text)));
|
||||||
|
if (replaceLastIndex_1 !== text.length) {
|
||||||
|
replaceResult_1 += text.substring(replaceLastIndex_1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
replaceResult_1 =
|
||||||
|
text;
|
||||||
|
}
|
||||||
|
return replaceResult_1;
|
||||||
|
}
|
||||||
|
exports.decode = decode;
|
|
@ -0,0 +1,51 @@
|
||||||
|
/**
|
||||||
|
* Flowtype definitions for index
|
||||||
|
* Generated by Flowgen from a Typescript Definition
|
||||||
|
* Flowgen v1.13.0
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type Level = "xml" | "html4" | "html5" | "all";
|
||||||
|
declare interface CommonOptions {
|
||||||
|
level?: Level;
|
||||||
|
}
|
||||||
|
export type EncodeMode =
|
||||||
|
| "specialChars"
|
||||||
|
| "nonAscii"
|
||||||
|
| "nonAsciiPrintable"
|
||||||
|
| "extensive";
|
||||||
|
export type EncodeOptions = {
|
||||||
|
mode?: EncodeMode,
|
||||||
|
numeric?: "decimal" | "hexadecimal",
|
||||||
|
...
|
||||||
|
} & CommonOptions;
|
||||||
|
export type DecodeScope = "strict" | "body" | "attribute";
|
||||||
|
export type DecodeOptions = {
|
||||||
|
scope?: DecodeScope,
|
||||||
|
...
|
||||||
|
} & CommonOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes all the necessary (specified by `level`) characters in the text
|
||||||
|
*/
|
||||||
|
declare export function encode(
|
||||||
|
text: string | void | null,
|
||||||
|
x?: EncodeOptions
|
||||||
|
): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes a single entity
|
||||||
|
*/
|
||||||
|
declare export function decodeEntity(
|
||||||
|
entity: string | void | null,
|
||||||
|
x?: CommonOptions
|
||||||
|
): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes all entities in the text
|
||||||
|
*/
|
||||||
|
declare export function decode(
|
||||||
|
text: string | void | null,
|
||||||
|
x?: DecodeOptions
|
||||||
|
): string;
|
||||||
|
declare export {};
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1 @@
|
||||||
|
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.numericUnicodeMap={0:65533,128:8364,130:8218,131:402,132:8222,133:8230,134:8224,135:8225,136:710,137:8240,138:352,139:8249,140:338,142:381,145:8216,146:8217,147:8220,148:8221,149:8226,150:8211,151:8212,152:732,153:8482,154:353,155:8250,156:339,158:382,159:376};
|
|
@ -0,0 +1 @@
|
||||||
|
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.fromCodePoint=String.fromCodePoint||function(astralCodePoint){return String.fromCharCode(Math.floor((astralCodePoint-65536)/1024)+55296,(astralCodePoint-65536)%1024+56320)};exports.getCodePoint=String.prototype.codePointAt?function(input,position){return input.codePointAt(position)}:function(input,position){return(input.charCodeAt(position)-55296)*1024+input.charCodeAt(position+1)-56320+65536};exports.highSurrogateFrom=55296;exports.highSurrogateTo=56319;
|
|
@ -0,0 +1,73 @@
|
||||||
|
{
|
||||||
|
"name": "html-entities",
|
||||||
|
"version": "2.3.2",
|
||||||
|
"description": "Fastest HTML entities encode/decode library.",
|
||||||
|
"keywords": [
|
||||||
|
"html",
|
||||||
|
"html entities",
|
||||||
|
"html entities encode",
|
||||||
|
"html entities decode",
|
||||||
|
"entities",
|
||||||
|
"entities encode",
|
||||||
|
"entities decode"
|
||||||
|
],
|
||||||
|
"author": {
|
||||||
|
"name": "Marat Dulin",
|
||||||
|
"email": "mdevils@yandex.ru"
|
||||||
|
},
|
||||||
|
"dependencies": {},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/benchmark": "^2.1.0",
|
||||||
|
"@types/chai": "^4.2.11",
|
||||||
|
"@types/he": "^1.1.1",
|
||||||
|
"@types/mocha": "^7.0.2",
|
||||||
|
"@types/node": "^13.13.4",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.6.1",
|
||||||
|
"@typescript-eslint/parser": "^4.6.1",
|
||||||
|
"benchmark": "^2.1.4",
|
||||||
|
"chai": "^4.2.0",
|
||||||
|
"entities": "^2.2.0",
|
||||||
|
"eslint": "^7.12.1",
|
||||||
|
"eslint-config-prettier": "^6.15.0",
|
||||||
|
"eslint-plugin-import": "^2.22.1",
|
||||||
|
"eslint-plugin-prettier": "^3.1.4",
|
||||||
|
"flowgen": "^1.13.0",
|
||||||
|
"he": "^1.2.0",
|
||||||
|
"husky": "^4.3.6",
|
||||||
|
"mocha": "^7.1.2",
|
||||||
|
"prettier": "^2.1.2",
|
||||||
|
"terser": "^5.6.1",
|
||||||
|
"ts-node": "^8.9.1",
|
||||||
|
"ttypescript": "^1.5.12",
|
||||||
|
"typescript": "^3.8.3",
|
||||||
|
"typescript-transform-macros": "^1.1.1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mdevils/html-entities.git"
|
||||||
|
},
|
||||||
|
"main": "./lib/index.js",
|
||||||
|
"typings": "./lib/index.d.ts",
|
||||||
|
"types": "./lib/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"test": "TS_NODE_COMPILER=ttypescript mocha --recursive -r ts-node/register test/**/*.ts",
|
||||||
|
"test:lib": "TEST_LIB=1 yarn test",
|
||||||
|
"benchmark": "TS_NODE_COMPILER=ttypescript ts-node benchmark/benchmark",
|
||||||
|
"lint": "eslint src/**.ts",
|
||||||
|
"flow-type-gen": "flowgen --add-flow-header lib/index.d.ts -o lib/index.js.flow",
|
||||||
|
"remove-unused-declarations": "find lib -type f \\( -name '*.d.ts' ! -name index.d.ts \\) | xargs rm",
|
||||||
|
"minimize-lib-files": "find lib -type f \\( -name '*.js' ! -name index.js \\) | while read fn; do terser $fn -o $fn; done",
|
||||||
|
"build": "rm -Rf lib/* && ttsc && yarn remove-unused-declarations && yarn flow-type-gen && yarn minimize-lib-files && yarn test:lib",
|
||||||
|
"prepublishOnly": "yarn build"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"lib",
|
||||||
|
"LICENSE"
|
||||||
|
],
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "yarn lint && yarn test"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
|
@ -1,50 +0,0 @@
|
||||||
# turndown-plugin-gfm
|
|
||||||
|
|
||||||
A [Turndown](https://github.com/domchristie/turndown) plugin which adds GitHub Flavored Markdown extensions.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
npm:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install turndown-plugin-gfm
|
|
||||||
```
|
|
||||||
|
|
||||||
Browser:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script src="https://unpkg.com/turndown/dist/turndown.js"></script>
|
|
||||||
<script src="https://unpkg.com/turndown-plugin-gfm/dist/turndown-plugin-gfm.js"></script>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```js
|
|
||||||
// For Node.js
|
|
||||||
var TurndownService = require('turndown')
|
|
||||||
var turndownPluginGfm = require('turndown-plugin-gfm')
|
|
||||||
|
|
||||||
var gfm = turndownPluginGfm.gfm
|
|
||||||
var turndownService = new TurndownService()
|
|
||||||
turndownService.use(gfm)
|
|
||||||
var markdown = turndownService.turndown('<strike>Hello world!</strike>')
|
|
||||||
```
|
|
||||||
|
|
||||||
turndown-plugin-gfm is a suite of plugins which can be applied individually. The available plugins are as follows:
|
|
||||||
|
|
||||||
- `strikethrough` (for converting `<strike>`, `<s>`, and `<del>` elements)
|
|
||||||
- `tables`
|
|
||||||
- `taskListItems`
|
|
||||||
- `gfm` (which applies all of the above)
|
|
||||||
|
|
||||||
So for example, if you only wish to convert tables:
|
|
||||||
|
|
||||||
```js
|
|
||||||
var tables = require('turndown-plugin-gfm').tables
|
|
||||||
var turndownService = new TurndownService()
|
|
||||||
turndownService.use(tables)
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
turndown-plugin-gfm is copyright © 2017+ Dom Christie and released under the MIT license.
|
|
|
@ -1,165 +0,0 @@
|
||||||
var turndownPluginGfm = (function (exports) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var highlightRegExp = /highlight-(?:text|source)-([a-z0-9]+)/;
|
|
||||||
|
|
||||||
function highlightedCodeBlock (turndownService) {
|
|
||||||
turndownService.addRule('highlightedCodeBlock', {
|
|
||||||
filter: function (node) {
|
|
||||||
var firstChild = node.firstChild;
|
|
||||||
return (
|
|
||||||
node.nodeName === 'DIV' &&
|
|
||||||
highlightRegExp.test(node.className) &&
|
|
||||||
firstChild &&
|
|
||||||
firstChild.nodeName === 'PRE'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
replacement: function (content, node, options) {
|
|
||||||
var className = node.className || '';
|
|
||||||
var language = (className.match(highlightRegExp) || [null, ''])[1];
|
|
||||||
|
|
||||||
return (
|
|
||||||
'\n\n' + options.fence + language + '\n' +
|
|
||||||
node.firstChild.textContent +
|
|
||||||
'\n' + options.fence + '\n\n'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function strikethrough (turndownService) {
|
|
||||||
turndownService.addRule('strikethrough', {
|
|
||||||
filter: ['del', 's', 'strike'],
|
|
||||||
replacement: function (content) {
|
|
||||||
return '~' + content + '~'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var indexOf = Array.prototype.indexOf;
|
|
||||||
var every = Array.prototype.every;
|
|
||||||
var rules = {};
|
|
||||||
|
|
||||||
rules.tableCell = {
|
|
||||||
filter: ['th', 'td'],
|
|
||||||
replacement: function (content, node) {
|
|
||||||
return cell(content, node)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
rules.tableRow = {
|
|
||||||
filter: 'tr',
|
|
||||||
replacement: function (content, node) {
|
|
||||||
var borderCells = '';
|
|
||||||
var alignMap = { left: ':--', right: '--:', center: ':-:' };
|
|
||||||
|
|
||||||
if (isHeadingRow(node)) {
|
|
||||||
for (var i = 0; i < node.childNodes.length; i++) {
|
|
||||||
var border = '---';
|
|
||||||
var align = (
|
|
||||||
node.childNodes[i].getAttribute('align') || ''
|
|
||||||
).toLowerCase();
|
|
||||||
|
|
||||||
if (align) border = alignMap[align] || border;
|
|
||||||
|
|
||||||
borderCells += cell(border, node.childNodes[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '\n' + content + (borderCells ? '\n' + borderCells : '')
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
rules.table = {
|
|
||||||
// Only convert tables with a heading row.
|
|
||||||
// Tables with no heading row are kept using `keep` (see below).
|
|
||||||
filter: function (node) {
|
|
||||||
return node.nodeName === 'TABLE' && isHeadingRow(node.rows[0])
|
|
||||||
},
|
|
||||||
|
|
||||||
replacement: function (content) {
|
|
||||||
// Ensure there are no blank lines
|
|
||||||
content = content.replace('\n\n', '\n');
|
|
||||||
return '\n\n' + content + '\n\n'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
rules.tableSection = {
|
|
||||||
filter: ['thead', 'tbody', 'tfoot'],
|
|
||||||
replacement: function (content) {
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// A tr is a heading row if:
|
|
||||||
// - the parent is a THEAD
|
|
||||||
// - or if its the first child of the TABLE or the first TBODY (possibly
|
|
||||||
// following a blank THEAD)
|
|
||||||
// - and every cell is a TH
|
|
||||||
function isHeadingRow (tr) {
|
|
||||||
var parentNode = tr.parentNode;
|
|
||||||
return (
|
|
||||||
parentNode.nodeName === 'THEAD' ||
|
|
||||||
(
|
|
||||||
parentNode.firstChild === tr &&
|
|
||||||
(parentNode.nodeName === 'TABLE' || isFirstTbody(parentNode)) &&
|
|
||||||
every.call(tr.childNodes, function (n) { return n.nodeName === 'TH' })
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isFirstTbody (element) {
|
|
||||||
var previousSibling = element.previousSibling;
|
|
||||||
return (
|
|
||||||
element.nodeName === 'TBODY' && (
|
|
||||||
!previousSibling ||
|
|
||||||
(
|
|
||||||
previousSibling.nodeName === 'THEAD' &&
|
|
||||||
/^\s*$/i.test(previousSibling.textContent)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function cell (content, node) {
|
|
||||||
var index = indexOf.call(node.parentNode.childNodes, node);
|
|
||||||
var prefix = ' ';
|
|
||||||
if (index === 0) prefix = '| ';
|
|
||||||
return prefix + content + ' |'
|
|
||||||
}
|
|
||||||
|
|
||||||
function tables (turndownService) {
|
|
||||||
turndownService.keep(function (node) {
|
|
||||||
return node.nodeName === 'TABLE' && !isHeadingRow(node.rows[0])
|
|
||||||
});
|
|
||||||
for (var key in rules) turndownService.addRule(key, rules[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function taskListItems (turndownService) {
|
|
||||||
turndownService.addRule('taskListItems', {
|
|
||||||
filter: function (node) {
|
|
||||||
return node.type === 'checkbox' && node.parentNode.nodeName === 'LI'
|
|
||||||
},
|
|
||||||
replacement: function (content, node) {
|
|
||||||
return (node.checked ? '[x]' : '[ ]') + ' '
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function gfm (turndownService) {
|
|
||||||
turndownService.use([
|
|
||||||
highlightedCodeBlock,
|
|
||||||
strikethrough,
|
|
||||||
tables,
|
|
||||||
taskListItems
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.gfm = gfm;
|
|
||||||
exports.highlightedCodeBlock = highlightedCodeBlock;
|
|
||||||
exports.strikethrough = strikethrough;
|
|
||||||
exports.tables = tables;
|
|
||||||
exports.taskListItems = taskListItems;
|
|
||||||
|
|
||||||
return exports;
|
|
||||||
|
|
||||||
}({}));
|
|
|
@ -1,162 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
Object.defineProperty(exports, '__esModule', { value: true });
|
|
||||||
|
|
||||||
var highlightRegExp = /highlight-(?:text|source)-([a-z0-9]+)/;
|
|
||||||
|
|
||||||
function highlightedCodeBlock (turndownService) {
|
|
||||||
turndownService.addRule('highlightedCodeBlock', {
|
|
||||||
filter: function (node) {
|
|
||||||
var firstChild = node.firstChild;
|
|
||||||
return (
|
|
||||||
node.nodeName === 'DIV' &&
|
|
||||||
highlightRegExp.test(node.className) &&
|
|
||||||
firstChild &&
|
|
||||||
firstChild.nodeName === 'PRE'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
replacement: function (content, node, options) {
|
|
||||||
var className = node.className || '';
|
|
||||||
var language = (className.match(highlightRegExp) || [null, ''])[1];
|
|
||||||
|
|
||||||
return (
|
|
||||||
'\n\n' + options.fence + language + '\n' +
|
|
||||||
node.firstChild.textContent +
|
|
||||||
'\n' + options.fence + '\n\n'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function strikethrough (turndownService) {
|
|
||||||
turndownService.addRule('strikethrough', {
|
|
||||||
filter: ['del', 's', 'strike'],
|
|
||||||
replacement: function (content) {
|
|
||||||
return '~' + content + '~'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var indexOf = Array.prototype.indexOf;
|
|
||||||
var every = Array.prototype.every;
|
|
||||||
var rules = {};
|
|
||||||
|
|
||||||
rules.tableCell = {
|
|
||||||
filter: ['th', 'td'],
|
|
||||||
replacement: function (content, node) {
|
|
||||||
return cell(content, node)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
rules.tableRow = {
|
|
||||||
filter: 'tr',
|
|
||||||
replacement: function (content, node) {
|
|
||||||
var borderCells = '';
|
|
||||||
var alignMap = { left: ':--', right: '--:', center: ':-:' };
|
|
||||||
|
|
||||||
if (isHeadingRow(node)) {
|
|
||||||
for (var i = 0; i < node.childNodes.length; i++) {
|
|
||||||
var border = '---';
|
|
||||||
var align = (
|
|
||||||
node.childNodes[i].getAttribute('align') || ''
|
|
||||||
).toLowerCase();
|
|
||||||
|
|
||||||
if (align) border = alignMap[align] || border;
|
|
||||||
|
|
||||||
borderCells += cell(border, node.childNodes[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '\n' + content + (borderCells ? '\n' + borderCells : '')
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
rules.table = {
|
|
||||||
// Only convert tables with a heading row.
|
|
||||||
// Tables with no heading row are kept using `keep` (see below).
|
|
||||||
filter: function (node) {
|
|
||||||
return node.nodeName === 'TABLE' && isHeadingRow(node.rows[0])
|
|
||||||
},
|
|
||||||
|
|
||||||
replacement: function (content) {
|
|
||||||
// Ensure there are no blank lines
|
|
||||||
content = content.replace('\n\n', '\n');
|
|
||||||
return '\n\n' + content + '\n\n'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
rules.tableSection = {
|
|
||||||
filter: ['thead', 'tbody', 'tfoot'],
|
|
||||||
replacement: function (content) {
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// A tr is a heading row if:
|
|
||||||
// - the parent is a THEAD
|
|
||||||
// - or if its the first child of the TABLE or the first TBODY (possibly
|
|
||||||
// following a blank THEAD)
|
|
||||||
// - and every cell is a TH
|
|
||||||
function isHeadingRow (tr) {
|
|
||||||
var parentNode = tr.parentNode;
|
|
||||||
return (
|
|
||||||
parentNode.nodeName === 'THEAD' ||
|
|
||||||
(
|
|
||||||
parentNode.firstChild === tr &&
|
|
||||||
(parentNode.nodeName === 'TABLE' || isFirstTbody(parentNode)) &&
|
|
||||||
every.call(tr.childNodes, function (n) { return n.nodeName === 'TH' })
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isFirstTbody (element) {
|
|
||||||
var previousSibling = element.previousSibling;
|
|
||||||
return (
|
|
||||||
element.nodeName === 'TBODY' && (
|
|
||||||
!previousSibling ||
|
|
||||||
(
|
|
||||||
previousSibling.nodeName === 'THEAD' &&
|
|
||||||
/^\s*$/i.test(previousSibling.textContent)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function cell (content, node) {
|
|
||||||
var index = indexOf.call(node.parentNode.childNodes, node);
|
|
||||||
var prefix = ' ';
|
|
||||||
if (index === 0) prefix = '| ';
|
|
||||||
return prefix + content + ' |'
|
|
||||||
}
|
|
||||||
|
|
||||||
function tables (turndownService) {
|
|
||||||
turndownService.keep(function (node) {
|
|
||||||
return node.nodeName === 'TABLE' && !isHeadingRow(node.rows[0])
|
|
||||||
});
|
|
||||||
for (var key in rules) turndownService.addRule(key, rules[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function taskListItems (turndownService) {
|
|
||||||
turndownService.addRule('taskListItems', {
|
|
||||||
filter: function (node) {
|
|
||||||
return node.type === 'checkbox' && node.parentNode.nodeName === 'LI'
|
|
||||||
},
|
|
||||||
replacement: function (content, node) {
|
|
||||||
return (node.checked ? '[x]' : '[ ]') + ' '
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function gfm (turndownService) {
|
|
||||||
turndownService.use([
|
|
||||||
highlightedCodeBlock,
|
|
||||||
strikethrough,
|
|
||||||
tables,
|
|
||||||
taskListItems
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.gfm = gfm;
|
|
||||||
exports.highlightedCodeBlock = highlightedCodeBlock;
|
|
||||||
exports.strikethrough = strikethrough;
|
|
||||||
exports.tables = tables;
|
|
||||||
exports.taskListItems = taskListItems;
|
|
|
@ -1,154 +0,0 @@
|
||||||
var highlightRegExp = /highlight-(?:text|source)-([a-z0-9]+)/;
|
|
||||||
|
|
||||||
function highlightedCodeBlock (turndownService) {
|
|
||||||
turndownService.addRule('highlightedCodeBlock', {
|
|
||||||
filter: function (node) {
|
|
||||||
var firstChild = node.firstChild;
|
|
||||||
return (
|
|
||||||
node.nodeName === 'DIV' &&
|
|
||||||
highlightRegExp.test(node.className) &&
|
|
||||||
firstChild &&
|
|
||||||
firstChild.nodeName === 'PRE'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
replacement: function (content, node, options) {
|
|
||||||
var className = node.className || '';
|
|
||||||
var language = (className.match(highlightRegExp) || [null, ''])[1];
|
|
||||||
|
|
||||||
return (
|
|
||||||
'\n\n' + options.fence + language + '\n' +
|
|
||||||
node.firstChild.textContent +
|
|
||||||
'\n' + options.fence + '\n\n'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function strikethrough (turndownService) {
|
|
||||||
turndownService.addRule('strikethrough', {
|
|
||||||
filter: ['del', 's', 'strike'],
|
|
||||||
replacement: function (content) {
|
|
||||||
return '~' + content + '~'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var indexOf = Array.prototype.indexOf;
|
|
||||||
var every = Array.prototype.every;
|
|
||||||
var rules = {};
|
|
||||||
|
|
||||||
rules.tableCell = {
|
|
||||||
filter: ['th', 'td'],
|
|
||||||
replacement: function (content, node) {
|
|
||||||
return cell(content, node)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
rules.tableRow = {
|
|
||||||
filter: 'tr',
|
|
||||||
replacement: function (content, node) {
|
|
||||||
var borderCells = '';
|
|
||||||
var alignMap = { left: ':--', right: '--:', center: ':-:' };
|
|
||||||
|
|
||||||
if (isHeadingRow(node)) {
|
|
||||||
for (var i = 0; i < node.childNodes.length; i++) {
|
|
||||||
var border = '---';
|
|
||||||
var align = (
|
|
||||||
node.childNodes[i].getAttribute('align') || ''
|
|
||||||
).toLowerCase();
|
|
||||||
|
|
||||||
if (align) border = alignMap[align] || border;
|
|
||||||
|
|
||||||
borderCells += cell(border, node.childNodes[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '\n' + content + (borderCells ? '\n' + borderCells : '')
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
rules.table = {
|
|
||||||
// Only convert tables with a heading row.
|
|
||||||
// Tables with no heading row are kept using `keep` (see below).
|
|
||||||
filter: function (node) {
|
|
||||||
return node.nodeName === 'TABLE' && isHeadingRow(node.rows[0])
|
|
||||||
},
|
|
||||||
|
|
||||||
replacement: function (content) {
|
|
||||||
// Ensure there are no blank lines
|
|
||||||
content = content.replace('\n\n', '\n');
|
|
||||||
return '\n\n' + content + '\n\n'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
rules.tableSection = {
|
|
||||||
filter: ['thead', 'tbody', 'tfoot'],
|
|
||||||
replacement: function (content) {
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// A tr is a heading row if:
|
|
||||||
// - the parent is a THEAD
|
|
||||||
// - or if its the first child of the TABLE or the first TBODY (possibly
|
|
||||||
// following a blank THEAD)
|
|
||||||
// - and every cell is a TH
|
|
||||||
function isHeadingRow (tr) {
|
|
||||||
var parentNode = tr.parentNode;
|
|
||||||
return (
|
|
||||||
parentNode.nodeName === 'THEAD' ||
|
|
||||||
(
|
|
||||||
parentNode.firstChild === tr &&
|
|
||||||
(parentNode.nodeName === 'TABLE' || isFirstTbody(parentNode)) &&
|
|
||||||
every.call(tr.childNodes, function (n) { return n.nodeName === 'TH' })
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isFirstTbody (element) {
|
|
||||||
var previousSibling = element.previousSibling;
|
|
||||||
return (
|
|
||||||
element.nodeName === 'TBODY' && (
|
|
||||||
!previousSibling ||
|
|
||||||
(
|
|
||||||
previousSibling.nodeName === 'THEAD' &&
|
|
||||||
/^\s*$/i.test(previousSibling.textContent)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function cell (content, node) {
|
|
||||||
var index = indexOf.call(node.parentNode.childNodes, node);
|
|
||||||
var prefix = ' ';
|
|
||||||
if (index === 0) prefix = '| ';
|
|
||||||
return prefix + content + ' |'
|
|
||||||
}
|
|
||||||
|
|
||||||
function tables (turndownService) {
|
|
||||||
turndownService.keep(function (node) {
|
|
||||||
return node.nodeName === 'TABLE' && !isHeadingRow(node.rows[0])
|
|
||||||
});
|
|
||||||
for (var key in rules) turndownService.addRule(key, rules[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function taskListItems (turndownService) {
|
|
||||||
turndownService.addRule('taskListItems', {
|
|
||||||
filter: function (node) {
|
|
||||||
return node.type === 'checkbox' && node.parentNode.nodeName === 'LI'
|
|
||||||
},
|
|
||||||
replacement: function (content, node) {
|
|
||||||
return (node.checked ? '[x]' : '[ ]') + ' '
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function gfm (turndownService) {
|
|
||||||
turndownService.use([
|
|
||||||
highlightedCodeBlock,
|
|
||||||
strikethrough,
|
|
||||||
tables,
|
|
||||||
taskListItems
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { gfm, highlightedCodeBlock, strikethrough, tables, taskListItems };
|
|
|
@ -1,162 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
Object.defineProperty(exports, '__esModule', { value: true });
|
|
||||||
|
|
||||||
var highlightRegExp = /highlight-(?:text|source)-([a-z0-9]+)/;
|
|
||||||
|
|
||||||
function highlightedCodeBlock (turndownService) {
|
|
||||||
turndownService.addRule('highlightedCodeBlock', {
|
|
||||||
filter: function (node) {
|
|
||||||
var firstChild = node.firstChild;
|
|
||||||
return (
|
|
||||||
node.nodeName === 'DIV' &&
|
|
||||||
highlightRegExp.test(node.className) &&
|
|
||||||
firstChild &&
|
|
||||||
firstChild.nodeName === 'PRE'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
replacement: function (content, node, options) {
|
|
||||||
var className = node.className || '';
|
|
||||||
var language = (className.match(highlightRegExp) || [null, ''])[1];
|
|
||||||
|
|
||||||
return (
|
|
||||||
'\n\n' + options.fence + language + '\n' +
|
|
||||||
node.firstChild.textContent +
|
|
||||||
'\n' + options.fence + '\n\n'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function strikethrough (turndownService) {
|
|
||||||
turndownService.addRule('strikethrough', {
|
|
||||||
filter: ['del', 's', 'strike'],
|
|
||||||
replacement: function (content) {
|
|
||||||
return '~' + content + '~'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var indexOf = Array.prototype.indexOf;
|
|
||||||
var every = Array.prototype.every;
|
|
||||||
var rules = {};
|
|
||||||
|
|
||||||
rules.tableCell = {
|
|
||||||
filter: ['th', 'td'],
|
|
||||||
replacement: function (content, node) {
|
|
||||||
return cell(content, node)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
rules.tableRow = {
|
|
||||||
filter: 'tr',
|
|
||||||
replacement: function (content, node) {
|
|
||||||
var borderCells = '';
|
|
||||||
var alignMap = { left: ':--', right: '--:', center: ':-:' };
|
|
||||||
|
|
||||||
if (isHeadingRow(node)) {
|
|
||||||
for (var i = 0; i < node.childNodes.length; i++) {
|
|
||||||
var border = '---';
|
|
||||||
var align = (
|
|
||||||
node.childNodes[i].getAttribute('align') || ''
|
|
||||||
).toLowerCase();
|
|
||||||
|
|
||||||
if (align) border = alignMap[align] || border;
|
|
||||||
|
|
||||||
borderCells += cell(border, node.childNodes[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '\n' + content + (borderCells ? '\n' + borderCells : '')
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
rules.table = {
|
|
||||||
// Only convert tables with a heading row.
|
|
||||||
// Tables with no heading row are kept using `keep` (see below).
|
|
||||||
filter: function (node) {
|
|
||||||
return node.nodeName === 'TABLE' && isHeadingRow(node.rows[0])
|
|
||||||
},
|
|
||||||
|
|
||||||
replacement: function (content) {
|
|
||||||
// Ensure there are no blank lines
|
|
||||||
content = content.replace('\n\n', '\n');
|
|
||||||
return '\n\n' + content + '\n\n'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
rules.tableSection = {
|
|
||||||
filter: ['thead', 'tbody', 'tfoot'],
|
|
||||||
replacement: function (content) {
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// A tr is a heading row if:
|
|
||||||
// - the parent is a THEAD
|
|
||||||
// - or if its the first child of the TABLE or the first TBODY (possibly
|
|
||||||
// following a blank THEAD)
|
|
||||||
// - and every cell is a TH
|
|
||||||
function isHeadingRow (tr) {
|
|
||||||
var parentNode = tr.parentNode;
|
|
||||||
return (
|
|
||||||
parentNode.nodeName === 'THEAD' ||
|
|
||||||
(
|
|
||||||
parentNode.firstChild === tr &&
|
|
||||||
(parentNode.nodeName === 'TABLE' || isFirstTbody(parentNode)) &&
|
|
||||||
every.call(tr.childNodes, function (n) { return n.nodeName === 'TH' })
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isFirstTbody (element) {
|
|
||||||
var previousSibling = element.previousSibling;
|
|
||||||
return (
|
|
||||||
element.nodeName === 'TBODY' && (
|
|
||||||
!previousSibling ||
|
|
||||||
(
|
|
||||||
previousSibling.nodeName === 'THEAD' &&
|
|
||||||
/^\s*$/i.test(previousSibling.textContent)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function cell (content, node) {
|
|
||||||
var index = indexOf.call(node.parentNode.childNodes, node);
|
|
||||||
var prefix = ' ';
|
|
||||||
if (index === 0) prefix = '| ';
|
|
||||||
return prefix + content + ' |'
|
|
||||||
}
|
|
||||||
|
|
||||||
function tables (turndownService) {
|
|
||||||
turndownService.keep(function (node) {
|
|
||||||
return node.nodeName === 'TABLE' && !isHeadingRow(node.rows[0])
|
|
||||||
});
|
|
||||||
for (var key in rules) turndownService.addRule(key, rules[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function taskListItems (turndownService) {
|
|
||||||
turndownService.addRule('taskListItems', {
|
|
||||||
filter: function (node) {
|
|
||||||
return node.type === 'checkbox' && node.parentNode.nodeName === 'LI'
|
|
||||||
},
|
|
||||||
replacement: function (content, node) {
|
|
||||||
return (node.checked ? '[x]' : '[ ]') + ' '
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function gfm (turndownService) {
|
|
||||||
turndownService.use([
|
|
||||||
highlightedCodeBlock,
|
|
||||||
strikethrough,
|
|
||||||
tables,
|
|
||||||
taskListItems
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.gfm = gfm;
|
|
||||||
exports.highlightedCodeBlock = highlightedCodeBlock;
|
|
||||||
exports.strikethrough = strikethrough;
|
|
||||||
exports.tables = tables;
|
|
||||||
exports.taskListItems = taskListItems;
|
|
|
@ -1,154 +0,0 @@
|
||||||
var highlightRegExp = /highlight-(?:text|source)-([a-z0-9]+)/;
|
|
||||||
|
|
||||||
function highlightedCodeBlock (turndownService) {
|
|
||||||
turndownService.addRule('highlightedCodeBlock', {
|
|
||||||
filter: function (node) {
|
|
||||||
var firstChild = node.firstChild;
|
|
||||||
return (
|
|
||||||
node.nodeName === 'DIV' &&
|
|
||||||
highlightRegExp.test(node.className) &&
|
|
||||||
firstChild &&
|
|
||||||
firstChild.nodeName === 'PRE'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
replacement: function (content, node, options) {
|
|
||||||
var className = node.className || '';
|
|
||||||
var language = (className.match(highlightRegExp) || [null, ''])[1];
|
|
||||||
|
|
||||||
return (
|
|
||||||
'\n\n' + options.fence + language + '\n' +
|
|
||||||
node.firstChild.textContent +
|
|
||||||
'\n' + options.fence + '\n\n'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function strikethrough (turndownService) {
|
|
||||||
turndownService.addRule('strikethrough', {
|
|
||||||
filter: ['del', 's', 'strike'],
|
|
||||||
replacement: function (content) {
|
|
||||||
return '~' + content + '~'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var indexOf = Array.prototype.indexOf;
|
|
||||||
var every = Array.prototype.every;
|
|
||||||
var rules = {};
|
|
||||||
|
|
||||||
rules.tableCell = {
|
|
||||||
filter: ['th', 'td'],
|
|
||||||
replacement: function (content, node) {
|
|
||||||
return cell(content, node)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
rules.tableRow = {
|
|
||||||
filter: 'tr',
|
|
||||||
replacement: function (content, node) {
|
|
||||||
var borderCells = '';
|
|
||||||
var alignMap = { left: ':--', right: '--:', center: ':-:' };
|
|
||||||
|
|
||||||
if (isHeadingRow(node)) {
|
|
||||||
for (var i = 0; i < node.childNodes.length; i++) {
|
|
||||||
var border = '---';
|
|
||||||
var align = (
|
|
||||||
node.childNodes[i].getAttribute('align') || ''
|
|
||||||
).toLowerCase();
|
|
||||||
|
|
||||||
if (align) border = alignMap[align] || border;
|
|
||||||
|
|
||||||
borderCells += cell(border, node.childNodes[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '\n' + content + (borderCells ? '\n' + borderCells : '')
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
rules.table = {
|
|
||||||
// Only convert tables with a heading row.
|
|
||||||
// Tables with no heading row are kept using `keep` (see below).
|
|
||||||
filter: function (node) {
|
|
||||||
return node.nodeName === 'TABLE' && isHeadingRow(node.rows[0])
|
|
||||||
},
|
|
||||||
|
|
||||||
replacement: function (content) {
|
|
||||||
// Ensure there are no blank lines
|
|
||||||
content = content.replace('\n\n', '\n');
|
|
||||||
return '\n\n' + content + '\n\n'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
rules.tableSection = {
|
|
||||||
filter: ['thead', 'tbody', 'tfoot'],
|
|
||||||
replacement: function (content) {
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// A tr is a heading row if:
|
|
||||||
// - the parent is a THEAD
|
|
||||||
// - or if its the first child of the TABLE or the first TBODY (possibly
|
|
||||||
// following a blank THEAD)
|
|
||||||
// - and every cell is a TH
|
|
||||||
function isHeadingRow (tr) {
|
|
||||||
var parentNode = tr.parentNode;
|
|
||||||
return (
|
|
||||||
parentNode.nodeName === 'THEAD' ||
|
|
||||||
(
|
|
||||||
parentNode.firstChild === tr &&
|
|
||||||
(parentNode.nodeName === 'TABLE' || isFirstTbody(parentNode)) &&
|
|
||||||
every.call(tr.childNodes, function (n) { return n.nodeName === 'TH' })
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isFirstTbody (element) {
|
|
||||||
var previousSibling = element.previousSibling;
|
|
||||||
return (
|
|
||||||
element.nodeName === 'TBODY' && (
|
|
||||||
!previousSibling ||
|
|
||||||
(
|
|
||||||
previousSibling.nodeName === 'THEAD' &&
|
|
||||||
/^\s*$/i.test(previousSibling.textContent)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function cell (content, node) {
|
|
||||||
var index = indexOf.call(node.parentNode.childNodes, node);
|
|
||||||
var prefix = ' ';
|
|
||||||
if (index === 0) prefix = '| ';
|
|
||||||
return prefix + content + ' |'
|
|
||||||
}
|
|
||||||
|
|
||||||
function tables (turndownService) {
|
|
||||||
turndownService.keep(function (node) {
|
|
||||||
return node.nodeName === 'TABLE' && !isHeadingRow(node.rows[0])
|
|
||||||
});
|
|
||||||
for (var key in rules) turndownService.addRule(key, rules[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function taskListItems (turndownService) {
|
|
||||||
turndownService.addRule('taskListItems', {
|
|
||||||
filter: function (node) {
|
|
||||||
return node.type === 'checkbox' && node.parentNode.nodeName === 'LI'
|
|
||||||
},
|
|
||||||
replacement: function (content, node) {
|
|
||||||
return (node.checked ? '[x]' : '[ ]') + ' '
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function gfm (turndownService) {
|
|
||||||
turndownService.use([
|
|
||||||
highlightedCodeBlock,
|
|
||||||
strikethrough,
|
|
||||||
tables,
|
|
||||||
taskListItems
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { gfm, highlightedCodeBlock, strikethrough, tables, taskListItems };
|
|
|
@ -1,43 +0,0 @@
|
||||||
{
|
|
||||||
"name": "turndown-plugin-gfm",
|
|
||||||
"description": "Turndown plugin to add GitHub Flavored Markdown extensions.",
|
|
||||||
"version": "1.0.2",
|
|
||||||
"author": "Dom Christie",
|
|
||||||
"main": "lib/turndown-plugin-gfm.cjs.js",
|
|
||||||
"module": "lib/turndown-plugin-gfm.es.js",
|
|
||||||
"jsnext:main": "lib/turndown-plugin-gfm.es.js",
|
|
||||||
"devDependencies": {
|
|
||||||
"browserify": "^14.5.0",
|
|
||||||
"rollup": "^0.50.0",
|
|
||||||
"standard": "^10.0.3",
|
|
||||||
"turndown": "4.0.1",
|
|
||||||
"turndown-attendant": "0.0.2"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"lib",
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"keywords": [
|
|
||||||
"turndown",
|
|
||||||
"turndown-plugin",
|
|
||||||
"html-to-markdown",
|
|
||||||
"html",
|
|
||||||
"markdown",
|
|
||||||
"github-flavored-markdown",
|
|
||||||
"gfm"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/domchristie/turndown-plugin-gfm.git"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "npm run build-cjs && npm run build-es && npm run build-iife && npm run build-test",
|
|
||||||
"build-cjs": "rollup -c config/rollup.config.cjs.js && rollup -c config/rollup.config.browser.cjs.js",
|
|
||||||
"build-es": "rollup -c config/rollup.config.es.js && rollup -c config/rollup.config.browser.es.js",
|
|
||||||
"build-iife": "rollup -c config/rollup.config.iife.js",
|
|
||||||
"build-test": "browserify test/turndown-plugin-gfm-test.js --outfile test/turndown-plugin-gfm-test.browser.js",
|
|
||||||
"prepublish": "npm run build",
|
|
||||||
"test": "npm run build && standard ./src/**/*.js && node test/turndown-plugin-gfm-test.js"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -11,12 +11,11 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@7c/validurl": "^0.0.3",
|
"@7c/validurl": "^0.0.3",
|
||||||
"@mozilla/readability": "^0.3.0",
|
"@mozilla/readability": "^0.3.0",
|
||||||
"body-parser": "^1.19.1",
|
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-rate-limit": "^6.0.5",
|
"express-rate-limit": "^6.0.5",
|
||||||
|
"html-entities": "^2.3.2",
|
||||||
"jsdom": "^16.4.0",
|
"jsdom": "^16.4.0",
|
||||||
"turndown": "^7.0.0",
|
"turndown": "^7.0.0",
|
||||||
"turndown-plugin-gfm": "^1.0.2",
|
|
||||||
"url": "^0.11.0"
|
"url": "^0.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -500,6 +499,11 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html-entities": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ=="
|
||||||
|
},
|
||||||
"node_modules/http-errors": {
|
"node_modules/http-errors": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
|
||||||
|
@ -1006,11 +1010,6 @@
|
||||||
"domino": "^2.1.6"
|
"domino": "^2.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/turndown-plugin-gfm": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/turndown-plugin-gfm/-/turndown-plugin-gfm-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg=="
|
|
||||||
},
|
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
|
||||||
|
@ -1539,6 +1538,11 @@
|
||||||
"whatwg-encoding": "^1.0.5"
|
"whatwg-encoding": "^1.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"html-entities": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ=="
|
||||||
|
},
|
||||||
"http-errors": {
|
"http-errors": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
|
||||||
|
@ -1913,11 +1917,6 @@
|
||||||
"domino": "^2.1.6"
|
"domino": "^2.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"turndown-plugin-gfm": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/turndown-plugin-gfm/-/turndown-plugin-gfm-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg=="
|
|
||||||
},
|
|
||||||
"type-check": {
|
"type-check": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
|
||||||
|
|
|
@ -6,12 +6,11 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@7c/validurl": "^0.0.3",
|
"@7c/validurl": "^0.0.3",
|
||||||
"@mozilla/readability": "^0.3.0",
|
"@mozilla/readability": "^0.3.0",
|
||||||
"body-parser": "^1.19.1",
|
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-rate-limit": "^6.0.5",
|
"express-rate-limit": "^6.0.5",
|
||||||
|
"html-entities": "^2.3.2",
|
||||||
"jsdom": "^16.4.0",
|
"jsdom": "^16.4.0",
|
||||||
"turndown": "^7.0.0",
|
"turndown": "^7.0.0",
|
||||||
"turndown-plugin-gfm": "^1.0.2",
|
|
||||||
"url": "^0.11.0"
|
"url": "^0.11.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
Loading…
Reference in New Issue