Add 'packages/turndown-plugin-gfm/' from commit '0f43299c17108e9508e7e6237369e86f504ea60a'

git-subtree-dir: packages/turndown-plugin-gfm
git-subtree-mainline: ebebdd0b07edda953fdc71b0003c223a6531e05c
git-subtree-split: 0f43299c17108e9508e7e6237369e86f504ea60a
This commit is contained in:
Elian Doran 2025-03-19 18:16:20 +02:00
commit d3423127d8
21 changed files with 4354 additions and 0 deletions

View File

@ -0,0 +1,5 @@
dist
lib
node_modules
npm-debug.log
test/*browser.js

View File

@ -0,0 +1,4 @@
language: node_js
node_js:
- "node"
- "6"

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Dom Christie
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,61 @@
# turndown-plugin-gfm
* * *
**IMPORTANT: This package is now hosted there: https://github.com/laurent22/joplin/tree/dev/packages/turndown-plugin-gfm**
It can be installed using `npm install --save @joplin/turndown-plugin-gfm`
* * *
A [Turndown](https://github.com/domchristie/turndown) plugin which adds GitHub Flavored Markdown extensions.
This is a fork of the original [turndown-plugin-gfm](https://github.com/domchristie/turndown-plugin-gfm) for use with [Joplin](https://github.com/laurent22/joplin). The changes are:
- New: Always render tables even if they don't have a header.
- New: Don't render the border of tables that contain other tables (frequent for websites that do the layout using tables). Only render the inner tables, if any, and if they also don't contain other tables.
- New: Replace newlines (`\n`) with `<br>` inside table cells so that multi-line content is displayed correctly as Markdown.
- New: Table cells are at least three characters long (padded with spaces) so that they render correctly in GFM-compliant renderers.
- New: Handle colspan in TD tags
- Fixed: Ensure there are no blank lines inside tables (due for example to an empty `<tr>` tag)
- Fixed: Fixed importing tables that contain pipes.
## Installation
npm:
```
npm install joplin-turndown-plugin-gfm
```
## Usage
```js
// For Node.js
var TurndownService = require('turndown')
var turndownPluginGfm = require('joplin-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.

View File

@ -0,0 +1,10 @@
#!/bin/bash
ROOT_DIR=/mnt/c/Users/laurent/src/joplin
CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
set -e
npm run build
# rsync -a ./dist/ $ROOT_DIR/ElectronClient/app/node_modules/joplin-turndown-plugin-gfm/dist/
# rsync -a ./lib/ $ROOT_DIR/ElectronClient/app/node_modules/joplin-turndown-plugin-gfm/lib/
rm -rf $ROOT_DIR/CliClient/node_modules/joplin-turndown-plugin-gfm
ln -s "$CURRENT_DIR" $ROOT_DIR/CliClient/node_modules/joplin-turndown-plugin-gfm
$ROOT_DIR/CliClient/run_test.sh HtmlToMd

View File

@ -0,0 +1,8 @@
import config from './rollup.config'
export default config({
output: {
format: 'cjs',
file: 'lib/turndown-plugin-gfm.browser.cjs.js'
}
})

View File

@ -0,0 +1,8 @@
import config from './rollup.config'
export default config({
output: {
format: 'es',
file: 'lib/turndown-plugin-gfm.browser.es.js'
}
})

View File

@ -0,0 +1,8 @@
import config from './rollup.config'
export default config({
output: {
format: 'cjs',
file: 'lib/turndown-plugin-gfm.cjs.js'
}
})

View File

@ -0,0 +1,8 @@
import config from './rollup.config'
export default config({
output: {
format: 'es',
file: 'lib/turndown-plugin-gfm.es.js'
}
})

View File

@ -0,0 +1,8 @@
import config from './rollup.config'
export default config({
output: {
format: 'iife',
file: 'dist/turndown-plugin-gfm.js'
}
})

View File

@ -0,0 +1,7 @@
export default function (config) {
return {
name: 'turndownPluginGfm',
input: 'src/gfm.js',
output: config.output
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
{
"name": "joplin-turndown-plugin-gfm",
"description": "Turndown plugin to add GitHub Flavored Markdown extensions.",
"version": "1.0.12",
"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/laurent22/joplin-turndown-plugin-gfm.git"
},
"scripts": {
"build": "npm run build-cjs && npm run build-es && npm run build-iife",
"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"
}
}

View File

@ -0,0 +1,3 @@
#!/bin/bash
npm version patch
npm publish

View File

@ -0,0 +1,15 @@
import highlightedCodeBlock from './highlighted-code-block'
import strikethrough from './strikethrough'
import tables from './tables'
import taskListItems from './task-list-items'
function gfm (turndownService) {
turndownService.use([
highlightedCodeBlock,
strikethrough,
tables,
taskListItems
])
}
export { gfm, highlightedCodeBlock, strikethrough, tables, taskListItems }

View File

@ -0,0 +1,25 @@
var highlightRegExp = /highlight-(?:text|source)-([a-z0-9]+)/
export default 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'
)
}
})
}

View File

@ -0,0 +1,8 @@
export default function strikethrough (turndownService) {
turndownService.addRule('strikethrough', {
filter: ['del', 's', 'strike'],
replacement: function (content) {
return '~' + content + '~'
}
})
}

View File

@ -0,0 +1,171 @@
var indexOf = Array.prototype.indexOf
var every = Array.prototype.every
var rules = {}
rules.tableCell = {
filter: ['th', 'td'],
replacement: function (content, node) {
if (tableShouldBeSkipped(nodeParentTable(node))) return content;
return cell(content, node)
}
}
rules.tableRow = {
filter: 'tr',
replacement: function (content, node) {
const parentTable = nodeParentTable(node);
if (tableShouldBeSkipped(parentTable)) return content;
var borderCells = ''
var alignMap = { left: ':--', right: '--:', center: ':-:' }
if (isHeadingRow(node)) {
const colCount = tableColCount(parentTable);
for (var i = 0; i < colCount; i++) {
const childNode = colCount >= node.childNodes.length ? null : node.childNodes[i];
var border = '---'
var align = childNode ? (childNode.getAttribute('align') || '').toLowerCase() : '';
if (align) border = alignMap[align] || border
if (childNode) {
borderCells += cell(border, node.childNodes[i])
} else {
borderCells += cell(border, null, 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'
},
replacement: function (content, node) {
if (tableShouldBeSkipped(node)) return content;
// Ensure there are no blank lines
content = content.replace(/\n+/g, '\n')
// If table has no heading, add an empty one so as to get a valid Markdown table
var secondLine = content.trim().split('\n');
if (secondLine.length >= 2) secondLine = secondLine[1]
var secondLineIsDivider = secondLine.indexOf('| ---') === 0
var columnCount = tableColCount(node);
var emptyHeader = ''
if (columnCount && !secondLineIsDivider) {
emptyHeader = '|' + ' |'.repeat(columnCount) + '\n' + '|' + ' --- |'.repeat(columnCount)
}
return '\n\n' + emptyHeader + 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 = null, index = null) {
if (index === null) index = indexOf.call(node.parentNode.childNodes, node)
var prefix = ' '
if (index === 0) prefix = '| '
let filteredContent = content.trim().replace(/\n\r/g, '<br>').replace(/\n/g, "<br>");
filteredContent = filteredContent.replace(/\|+/g, '\\|')
while (filteredContent.length < 3) filteredContent += ' ';
if (node) filteredContent = handleColSpan(filteredContent, node, ' ');
return prefix + filteredContent + ' |'
}
function nodeContainsTable(node) {
if (!node.childNodes) return false;
for (let i = 0; i < node.childNodes.length; i++) {
const child = node.childNodes[i];
if (child.nodeName === 'TABLE') return true;
if (nodeContainsTable(child)) return true;
}
return false;
}
// Various conditions under which a table should be skipped - i.e. each cell
// will be rendered one after the other as if they were paragraphs.
function tableShouldBeSkipped(tableNode) {
if (!tableNode) return true;
if (!tableNode.rows) return true;
if (tableNode.rows.length === 1 && tableNode.rows[0].childNodes.length <= 1) return true; // Table with only one cell
if (nodeContainsTable(tableNode)) return true;
return false;
}
function nodeParentTable(node) {
let parent = node.parentNode;
while (parent.nodeName !== 'TABLE') {
parent = parent.parentNode;
if (!parent) return null;
}
return parent;
}
function handleColSpan(content, node, emptyChar) {
const colspan = node.getAttribute('colspan') || 1;
for (let i = 1; i < colspan; i++) {
content += ' | ' + emptyChar.repeat(3);
}
return content
}
function tableColCount(node) {
let maxColCount = 0;
for (let i = 0; i < node.rows.length; i++) {
const row = node.rows[i]
const colCount = row.childNodes.length
if (colCount > maxColCount) maxColCount = colCount
}
return maxColCount
}
export default function tables (turndownService) {
turndownService.keep(function (node) {
return node.nodeName === 'TABLE'
})
for (var key in rules) turndownService.addRule(key, rules[key])
}

View File

@ -0,0 +1,10 @@
export default 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]' : '[ ]') + ' '
}
})
}

View File

@ -0,0 +1,323 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>turndown test runner</title>
<link rel="stylesheet" href="../node_modules/turndown-attendant/dist/styles.css">
</head>
<body>
<!-- TEST CASES -->
<div class="case" data-name="strike">
<div class="input"><strike>Lorem ipsum</strike></div>
<pre class="expected">~Lorem ipsum~</pre>
</div>
<div class="case" data-name="s">
<div class="input"><s>Lorem ipsum</s></div>
<pre class="expected">~Lorem ipsum~</pre>
</div>
<div class="case" data-name="del">
<div class="input"><del>Lorem ipsum</del></div>
<pre class="expected">~Lorem ipsum~</pre>
</div>
<div class="case" data-name="unchecked inputs">
<div class="input"><ul><li><input type=checkbox>Check Me!</li></ul></div>
<pre class="expected">* [ ] Check Me!</pre>
</div>
<div class="case" data-name="checked inputs">
<div class="input"><ul><li><input type=checkbox checked>Checked!</li></ul></div>
<pre class="expected">* [x] Checked!</pre>
</div>
<div class="case" data-name="basic table">
<div class="input">
<table>
<thead>
<tr>
<th>Column 1</th>
<th>Column 2</th>
</tr>
</thead>
<tbody>
<tr>
<td>Row 1, Column 1</td>
<td>Row 1, Column 2</td>
</tr>
<tr>
<td>Row 2, Column 1</td>
<td>Row 2, Column 2</td>
</tr>
</tbody>
</table>
</div>
<pre class="expected">| Column 1 | Column 2 |
| --- | --- |
| Row 1, Column 1 | Row 1, Column 2 |
| Row 2, Column 1 | Row 2, Column 2 |</pre>
</div>
<div class="case" data-name="cell alignment">
<div class="input">
<table>
<thead>
<tr>
<th align="left">Column 1</th>
<th align="center">Column 2</th>
<th align="right">Column 3</th>
<th align="foo">Column 4</th>
</tr>
</thead>
<tbody>
<tr>
<td>Row 1, Column 1</td>
<td>Row 1, Column 2</td>
<td>Row 1, Column 3</td>
<td>Row 1, Column 4</td>
</tr>
<tr>
<td>Row 2, Column 1</td>
<td>Row 2, Column 2</td>
<td>Row 2, Column 3</td>
<td>Row 2, Column 4</td>
</tr>
</tbody>
</table>
</div>
<pre class="expected">| Column 1 | Column 2 | Column 3 | Column 4 |
| :-- | :-: | --: | --- |
| Row 1, Column 1 | Row 1, Column 2 | Row 1, Column 3 | Row 1, Column 4 |
| Row 2, Column 1 | Row 2, Column 2 | Row 2, Column 3 | Row 2, Column 4 |</pre>
</div>
<div class="case" data-name="empty cells">
<div class="input">
<table>
<thead>
<tr>
<th align="left">Column 1</th>
<th align="center">Column 2</th>
<th align="right">Column 3</th>
<th align="foo">Column 4</th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
<td>Row 1, Column 2</td>
<td>Row 1, Column 3</td>
<td>Row 1, Column 4</td>
</tr>
<tr>
<td>Row 2, Column 1</td>
<td></td>
<td>Row 2, Column 3</td>
<td>Row 2, Column 4</td>
</tr>
<tr>
<td>Row 3, Column 1</td>
<td>Row 3, Column 2</td>
<td></td>
<td>Row 3, Column 4</td>
</tr>
<tr>
<td>Row 4, Column 1</td>
<td>Row 4, Column 2</td>
<td>Row 4, Column 3</td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td>Row 5, Column 4</td>
</tr>
</tbody>
</table>
</div>
<pre class="expected">| Column 1 | Column 2 | Column 3 | Column 4 |
| :-- | :-: | --: | --- |
| | Row 1, Column 2 | Row 1, Column 3 | Row 1, Column 4 |
| Row 2, Column 1 | | Row 2, Column 3 | Row 2, Column 4 |
| Row 3, Column 1 | Row 3, Column 2 | | Row 3, Column 4 |
| Row 4, Column 1 | Row 4, Column 2 | Row 4, Column 3 | |
| | | | Row 5, Column 4 |</pre>
</div>
<div class="case" data-name="empty rows">
<div class="input">
<table>
<thead>
<td>Heading 1</td>
<td>Heading 2</td>
</thead>
<tbody>
<tr>
<td>Row 1</td>
<td>Row 1</td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
<tr>
<td>Row 3</td>
<td>Row 3</td>
</tr>
</tbody>
</table>
</div>
<pre class="expected">| Heading 1 | Heading 2 |
| --- | --- |
| Row 1 | Row 1 |
| Row 3 | Row 3 |</pre>
</div>
<div class="case" data-name="th in first row">
<div class="input">
<table>
<tr>
<th>Heading</th>
</tr>
<tr>
<td>Content</td>
</tr>
</table>
</div>
<pre class="expected">| Heading |
| --- |
| Content |</pre>
</div>
<div class="case" data-name="th first row in tbody">
<div class="input">
<table>
<tbody>
<tr>
<th>Heading</th>
</tr>
<tr>
<td>Content</td>
</tr>
</tbody>
</table>
</div>
<pre class="expected">| Heading |
| --- |
| Content |</pre>
</div>
<div class="case" data-name="table with two tbodies">
<div class="input">
<table>
<tbody>
<tr>
<th>Heading</th>
</tr>
<tr>
<td>Content</td>
</tr>
</tbody>
<tbody>
<tr>
<th>Heading</th>
</tr>
<tr>
<td>Content</td>
</tr>
</tbody>
</table>
</div>
<pre class="expected">| Heading |
| --- |
| Content |
| Heading |
| Content |</pre>
</div>
<div class="case" data-name="heading cells in both thead and tbody">
<div class="input">
<table>
<thead><tr><th>Heading</th></tr></thead>
<tbody><tr><th>Cell</th></tr></tbody>
</table>
</div>
<pre class="expected">| Heading |
| --- |
| Cell |</pre>
</div>
<div class="case" data-name="empty head">
<div class="input">
<table>
<thead><tr><th></th></tr></thead>
<tbody><tr><th>Heading</th></tr></tbody>
</table>
</div>
<pre class="expected">| Heading |
| --- |</pre>
</div>
<div class="case" data-name="non-definitive heading row (converted but with empty header)">
<div class="input">
<table>
<tr><td>Row 1 Cell 1</td><td>Row 1 Cell 2</td></tr>
<tr><td>Row 2 Cell 1</td><td>Row 2 Cell 2</td></tr>
</table>
</div>
<pre class="expected">| | |
| --- | --- |
| Row 1 Cell 1 | Row 1 Cell 2 |
| Row 2 Cell 1 | Row 2 Cell 2 |</pre>
</div>
<div class="case" data-name="non-definitive heading row with th (converted but with empty header)">
<div class="input">
<table>
<tr>
<th>Heading</th>
<td>Not a heading</td>
</tr>
<tr>
<td>Heading</td>
<td>Not a heading</td>
</tr>
</table>
</div>
<pre class="expected">| | |
| --- | --- |
| Heading | Not a heading |
| Heading | Not a heading |</pre>
</div>
<div class="case" data-name="highlighted code block with html">
<div class="input">
<div class="highlight highlight-text-html-basic">
<pre>&lt;<span class="pl-ent">p</span>&gt;Hello world&lt;/<span class="pl-ent">p</span>&gt;</pre>
</div>
</div>
<pre class="expected">```html
&lt;p&gt;Hello world&lt;/p&gt;
```</pre>
</div>
<div class="case" data-name="highlighted code block with js">
<div class="input">
<div class="highlight highlight-source-js">
<pre>;(<span class="pl-k">function</span> () {})()</pre>
</div>
</div>
<pre class="expected">```js
;(function () {})()
```</pre>
</div>
<!-- /TEST CASES -->
<script src="turndown-plugin-gfm-test.browser.js"></script>
</body>
</html>

View File

@ -0,0 +1,13 @@
var Attendant = require('turndown-attendant')
var TurndownService = require('turndown')
var gfm = require('../lib/turndown-plugin-gfm.cjs').gfm
var attendant = new Attendant({
file: __dirname + '/index.html',
TurndownService: TurndownService,
beforeEach: function (turndownService) {
turndownService.use(gfm)
}
})
attendant.run()