chore(nx): prepare turndown-plugin-gfm

This commit is contained in:
Elian Doran
2025-04-22 15:20:41 +03:00
parent 41cf38a26c
commit a10a4ba17d
69 changed files with 5035 additions and 3692 deletions
+22
View File
@@ -0,0 +1,22 @@
{
"jsc": {
"target": "es2017",
"parser": {
"syntax": "typescript",
"decorators": true,
"dynamicImport": true
},
"transform": {
"decoratorMetadata": true,
"legacyDecorator": true
},
"keepClassNames": true,
"externalHelpers": true,
"loose": true
},
"module": {
"type": "es6"
},
"sourceMaps": true,
"exclude": ["jest.config.ts",".*\\.spec.tsx?$",".*\\.test.tsx?$","./src/jest-setup.ts$","./**/jest-setup.ts$",".*.js$"]
}
+21
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.
+7
View File
@@ -0,0 +1,7 @@
# turndown-plugin-gfm
This library was generated with [Nx](https://nx.dev).
## Building
Run `nx build turndown-plugin-gfm` to build the library.
+2
View File
@@ -0,0 +1,2 @@
export * from "./lib/gfm.js";
//# sourceMappingURL=index.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":""}
+7
View File
@@ -0,0 +1,7 @@
export function gfm(turndownService: any): void;
import highlightedCodeBlock from './highlighted-code-block.js';
import strikethrough from './strikethrough.js';
import tables from './tables.js';
import taskListItems from './task-list-items.js';
export { highlightedCodeBlock, strikethrough, tables, taskListItems };
//# sourceMappingURL=gfm.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"gfm.d.ts","sourceRoot":"","sources":["../../src/lib/gfm.js"],"names":[],"mappings":"AAKA,gDAOC;iCAZgC,6BAA6B;0BACpC,oBAAoB;mBAC3B,aAAa;0BACN,sBAAsB"}
@@ -0,0 +1,2 @@
export default function highlightedCodeBlock(turndownService: any): void;
//# sourceMappingURL=highlighted-code-block.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"highlighted-code-block.d.ts","sourceRoot":"","sources":["../../src/lib/highlighted-code-block.js"],"names":[],"mappings":"AAEA,yEAsBC"}
@@ -0,0 +1,2 @@
export default function strikethrough(turndownService: any): void;
//# sourceMappingURL=strikethrough.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"strikethrough.d.ts","sourceRoot":"","sources":["../../src/lib/strikethrough.js"],"names":[],"mappings":"AAAA,kEAOC"}
+2
View File
@@ -0,0 +1,2 @@
export default function tables(turndownService: any): void;
//# sourceMappingURL=tables.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../src/lib/tables.js"],"names":[],"mappings":"AAoRA,2DASC"}
@@ -0,0 +1,2 @@
export default function taskListItems(turndownService: any): void;
//# sourceMappingURL=task-list-items.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"task-list-items.d.ts","sourceRoot":"","sources":["../../src/lib/task-list-items.js"],"names":[],"mappings":"AAAA,kEASC"}
@@ -0,0 +1 @@
{"version":"5.7.3"}
+39
View File
@@ -0,0 +1,39 @@
{
"name": "@triliumnext/turndown-plugin-gfm",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"nx": {
"sourceRoot": "packages/turndown-plugin-gfm/src",
"targets": {
"build": {
"executor": "@nx/js:swc",
"outputs": [
"{options.outputPath}"
],
"options": {
"outputPath": "packages/turndown-plugin-gfm/dist",
"main": "packages/turndown-plugin-gfm/src/index.js",
"tsConfig": "packages/turndown-plugin-gfm/tsconfig.lib.json",
"skipTypeCheck": true,
"stripLeadingPaths": true
}
}
}
},
"dependencies": {
"@swc/helpers": "~0.5.11"
}
}
@@ -0,0 +1 @@
export * from './lib/gfm.js';
@@ -0,0 +1,15 @@
import highlightedCodeBlock from './highlighted-code-block.js'
import strikethrough from './strikethrough.js'
import tables from './tables.js'
import taskListItems from './task-list-items.js'
function gfm (turndownService) {
turndownService.use([
highlightedCodeBlock,
strikethrough,
tables,
taskListItems
])
}
export { gfm, highlightedCodeBlock, strikethrough, tables, taskListItems }
@@ -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'
)
}
})
}
@@ -0,0 +1,8 @@
export default function strikethrough (turndownService) {
turndownService.addRule('strikethrough', {
filter: ['del', 's', 'strike'],
replacement: function (content) {
return '~~' + content + '~~'
}
})
}
@@ -0,0 +1,286 @@
var indexOf = Array.prototype.indexOf
var every = Array.prototype.every
var rules = {}
var alignMap = { left: ':---', right: '---:', center: ':---:' };
let isCodeBlock_ = null;
let options_ = null;
// We need to cache the result of tableShouldBeSkipped() as it is expensive.
// Caching it means we went from about 9000 ms for rendering down to 90 ms.
// Fixes https://github.com/laurent22/joplin/issues/6736
const tableShouldBeSkippedCache_ = new WeakMap();
function getAlignment(node) {
return node ? (node.getAttribute('align') || node.style.textAlign || '').toLowerCase() : '';
}
function getBorder(alignment) {
return alignment ? alignMap[alignment] : '---';
}
function getColumnAlignment(table, columnIndex) {
var votes = {
left: 0,
right: 0,
center: 0,
'': 0,
};
var align = '';
for (var i = 0; i < table.rows.length; ++i) {
var row = table.rows[i];
if (columnIndex < row.childNodes.length) {
var cellAlignment = getAlignment(row.childNodes[columnIndex]);
++votes[cellAlignment];
if (votes[cellAlignment] > votes[align]) {
align = cellAlignment;
}
}
}
return align;
}
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 = ''
if (isHeadingRow(node)) {
const colCount = tableColCount(parentTable);
for (var i = 0; i < colCount; i++) {
const childNode = i < node.childNodes.length ? node.childNodes[i] : null;
var border = getBorder(getColumnAlignment(parentTable, i));
borderCells += cell(border, childNode, i);
}
}
return '\n' + content + (borderCells ? '\n' + borderCells : '')
}
}
rules.table = {
filter: function (node, options) {
return node.nodeName === 'TABLE';
},
replacement: function (content, node) {
// Only convert tables that can result in valid Markdown
// Other tables are kept as HTML using `keep` (see below).
if (tableShouldBeHtml(node, options_)) {
return node.outerHTML;
} else {
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 = /\| :?---/.test(secondLine);
var columnCount = tableColCount(node);
var emptyHeader = ''
if (columnCount && !secondLineIsDivider) {
emptyHeader = '|' + ' |'.repeat(columnCount) + '\n' + '|'
for (var columnIndex = 0; columnIndex < columnCount; ++columnIndex) {
emptyHeader += ' ' + getBorder(getColumnAlignment(node, columnIndex)) + ' |';
}
}
const captionContent = node.caption ? node.caption.textContent || '' : '';
const caption = captionContent ? `${captionContent}\n\n` : '';
const tableContent = `${emptyHeader}${content}`.trimStart();
return `\n\n${caption}${tableContent}\n\n`;
}
}
}
rules.tableCaption = {
filter: ['caption'],
replacement: () => '',
};
rules.tableColgroup = {
filter: ['colgroup', 'col'],
replacement: () => '',
};
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;
}
const nodeContains = (node, types) => {
if (!node.childNodes) return false;
for (let i = 0; i < node.childNodes.length; i++) {
const child = node.childNodes[i];
if (types === 'code' && isCodeBlock_ && isCodeBlock_(child)) return true;
if (types.includes(child.nodeName)) return true;
if (nodeContains(child, types)) return true;
}
return false;
}
const tableShouldBeHtml = (tableNode, options) => {
const possibleTags = [
'UL',
'OL',
'H1',
'H2',
'H3',
'H4',
'H5',
'H6',
'HR',
'BLOCKQUOTE',
'PRE'
];
// In general we should leave as HTML tables that include other tables. The
// exception is with the Web Clipper when we import a web page with a layout
// that's made of HTML tables. In that case we have this logic of removing the
// outer table and keeping only the inner ones. For the Rich Text editor
// however we always want to keep nested tables.
if (options.preserveNestedTables) possibleTags.push('TABLE');
return nodeContains(tableNode, 'code') ||
nodeContains(tableNode, possibleTags);
}
// 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) {
const cached = tableShouldBeSkippedCache_.get(tableNode);
if (cached !== undefined) return cached;
const result = tableShouldBeSkipped_(tableNode);
tableShouldBeSkippedCache_.set(tableNode, result);
return result;
}
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 nodeParentDiv(node) {
let parent = node.parentNode;
while (parent.nodeName !== 'DIV') {
parent = parent.parentNode;
if (!parent) return null;
}
return parent;
}
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) {
isCodeBlock_ = turndownService.isCodeBlock;
options_ = turndownService.options;
turndownService.keep(function (node) {
if (node.nodeName === 'TABLE' && tableShouldBeHtml(node, turndownService.options)) return true;
return false;
});
for (var key in rules) turndownService.addRule(key, rules[key])
}
@@ -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]' : '[ ]') + ' '
}
})
}
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
@@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"rootDir": "src",
"outDir": "dist",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
"emitDeclarationOnly": true,
"allowJs": true,
"forceConsistentCasingInFileNames": true,
"types": ["node"]
},
"include": ["src/**/*.ts", "src/**/*.js"],
"references": []
}