├── packages ├── docs │ ├── Demo.md │ ├── Example.md │ ├── zh-hans │ │ ├── 示例.md │ │ ├── 插件.md │ │ └── README.md │ ├── .vuepress │ │ ├── nav │ │ │ ├── zh.js │ │ │ └── en.js │ │ ├── enhanceApp.js │ │ ├── components │ │ │ ├── Examples.vue │ │ │ ├── Example.vue │ │ │ └── Demo.vue │ │ ├── config.js │ │ └── util │ │ │ └── highlight.js │ ├── package.json │ └── Plugin.md ├── core │ ├── test │ │ ├── globals.d.ts │ │ ├── spec │ │ │ ├── markdownPaper │ │ │ │ ├── table-no-header.md │ │ │ │ ├── table-no-header.html │ │ │ │ ├── paper2.md │ │ │ │ ├── paper2-csdn.md │ │ │ │ └── paper2-csdn.html │ │ │ └── md-it-plugin-taskList.js │ │ ├── table-no-header.test.ts │ │ ├── csdn.test.ts │ │ └── gfm.test.ts │ ├── src │ │ ├── util │ │ │ ├── repeat.ts │ │ │ ├── replacement │ │ │ │ ├── index.ts │ │ │ │ ├── keep.ts │ │ │ │ ├── list │ │ │ │ │ ├── index.ts │ │ │ │ │ └── listNode.ts │ │ │ │ ├── fence.ts │ │ │ │ └── blank.ts │ │ │ ├── escape.ts │ │ │ ├── isFence.ts │ │ │ ├── isCode.ts │ │ │ ├── indentCodeIsListfirstChild.ts │ │ │ ├── findParentNumber.ts │ │ │ ├── isKeep.ts │ │ │ ├── isVoid.ts │ │ │ ├── index.ts │ │ │ ├── findOrderListIndentNumber.ts │ │ │ ├── isBlock.ts │ │ │ ├── join.ts │ │ │ └── collapse-whitespace.ts │ │ ├── plugins │ │ │ ├── hr.ts │ │ │ ├── br.ts │ │ │ ├── del.ts │ │ │ ├── fencedCodeBlock.ts │ │ │ ├── strong.ts │ │ │ ├── image.ts │ │ │ ├── taskListItems.ts │ │ │ ├── blockquote.ts │ │ │ ├── em.ts │ │ │ ├── paragraph.ts │ │ │ ├── list.ts │ │ │ ├── heading.ts │ │ │ ├── code.ts │ │ │ ├── link.ts │ │ │ ├── indentedCodeBlock.ts │ │ │ ├── index.ts │ │ │ ├── referenceLinks.ts │ │ │ └── table.ts │ │ ├── service │ │ │ ├── Node │ │ │ │ ├── isBlank.ts │ │ │ │ ├── index.ts │ │ │ │ └── flankingWhitespace.ts │ │ │ ├── Rules │ │ │ │ ├── findRule.ts │ │ │ │ └── index.ts │ │ │ ├── HTMLParser.ts │ │ │ ├── RootNode.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ └── types │ │ │ └── index.ts │ ├── jest.config.js │ ├── tsconfig.json │ └── package.json └── @sitdown │ ├── juejin │ ├── test │ │ ├── globals.d.ts │ │ └── juejin.test.ts │ ├── jest.config.js │ ├── README.md │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── package.json │ ├── wechat │ ├── test │ │ ├── globals.d.ts │ │ ├── spec │ │ │ └── markdownPaper │ │ │ │ ├── fence.md │ │ │ │ ├── formula.md │ │ │ │ ├── fence.html │ │ │ │ ├── formula.html │ │ │ │ ├── paper4.md │ │ │ │ ├── paper2.md │ │ │ │ ├── paper5.md │ │ │ │ ├── paper3.md │ │ │ │ └── paper1.md │ │ └── wechat.test.ts │ ├── jest.config.js │ ├── README.md │ ├── tsconfig.json │ ├── package.json │ └── src │ │ ├── fence.ts │ │ └── index.ts │ ├── zhihu │ ├── test │ │ ├── globals.d.ts │ │ ├── spec │ │ │ └── markdownPaper │ │ │ │ ├── formula.md │ │ │ │ ├── formula.html │ │ │ │ ├── paper2-zhihu.md │ │ │ │ └── paper3-zhihu.md │ │ └── zhihu.test.ts │ ├── jest.config.js │ ├── README.md │ ├── src │ │ ├── p.ts │ │ └── index.ts │ ├── tsconfig.json │ └── package.json │ └── javascriptweekly │ ├── test │ ├── globals.d.ts │ ├── spec │ │ └── markdownPaper │ │ │ ├── paper1.md │ │ │ └── paper1.html │ └── javascriptweekly.test.ts │ ├── jest.config.js │ ├── README.md │ ├── tsdx.config.js │ ├── tsconfig.json │ ├── src │ └── index.ts │ └── package.json ├── lerna.json ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── main.yml │ └── dev.yml ├── LICENSE ├── package.json ├── .gitignore └── README.md /packages/docs/Demo.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/docs/Example.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/docs/zh-hans/示例.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/core/test/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.md'; 2 | declare module '*.html'; 3 | -------------------------------------------------------------------------------- /packages/@sitdown/juejin/test/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.md'; 2 | declare module '*.html'; 3 | -------------------------------------------------------------------------------- /packages/@sitdown/wechat/test/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.md'; 2 | declare module '*.html'; 3 | -------------------------------------------------------------------------------- /packages/@sitdown/zhihu/test/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.md'; 2 | declare module '*.html'; 3 | -------------------------------------------------------------------------------- /packages/@sitdown/zhihu/test/spec/markdownPaper/formula.md: -------------------------------------------------------------------------------- 1 | $$ 2 | -logp(y|x+r_{adv};\theta) \\ 3 | $$ -------------------------------------------------------------------------------- /packages/@sitdown/javascriptweekly/test/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.md'; 2 | declare module '*.html'; 3 | -------------------------------------------------------------------------------- /packages/@sitdown/wechat/test/spec/markdownPaper/fence.md: -------------------------------------------------------------------------------- 1 | ```css 2 | pre code { 3 | display: -webkit-box !important 4 | } 5 | ``` -------------------------------------------------------------------------------- /packages/core/src/util/repeat.ts: -------------------------------------------------------------------------------- 1 | export function repeat(character: string, count: number) { 2 | return Array(count + 1).join(character); 3 | } 4 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "1.1.1", 6 | "npmClient": "yarn", 7 | "useWorkspaces": true 8 | } 9 | -------------------------------------------------------------------------------- /packages/@sitdown/zhihu/test/spec/markdownPaper/formula.html: -------------------------------------------------------------------------------- 1 |

[公式]

-------------------------------------------------------------------------------- /packages/core/src/util/replacement/index.ts: -------------------------------------------------------------------------------- 1 | export { blankReplacement } from './blank'; 2 | export { listReplacement } from './list'; 3 | export { fenceReplacement } from './fence'; 4 | export { keepReplacement } from './keep'; 5 | -------------------------------------------------------------------------------- /packages/docs/.vuepress/nav/zh.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | {"link": "/zh-hans/示例/", "text": "示例"}, 3 | {"link": "/zh-hans/插件/", "text": "插件"}, 4 | {"link": "/Demo.html", "text": "Demo"}, 5 | {"link": "https://docschina.org/", "text": "印记中文"}, 6 | ] 7 | -------------------------------------------------------------------------------- /packages/docs/.vuepress/nav/en.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | {"link": "/Example.html", "text": "Example"}, 3 | {"link": "/Plugin.html", "text": "Plugin"}, 4 | {"link": "/Demo.html", "text": "Demo"}, 5 | {"link": "https://docschina.org/", "text": "docschina"}, 6 | ] 7 | -------------------------------------------------------------------------------- /packages/core/src/plugins/hr.ts: -------------------------------------------------------------------------------- 1 | import Service from '../service'; 2 | 3 | export const applyHrRule = (service: Service) => { 4 | service.addRule('hr', { 5 | filter: 'hr', 6 | 7 | replacement: function(_, __, options) { 8 | return '\n' + options.hr + '\n'; 9 | }, 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/core/src/plugins/br.ts: -------------------------------------------------------------------------------- 1 | import Service from '../service'; 2 | 3 | export const applyBrRule = (service: Service) => { 4 | service.addRule('hr', { 5 | filter: 'br', 6 | 7 | replacement: function(_content, _node, options) { 8 | return options.br + '\n'; 9 | }, 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/core/src/util/escape.ts: -------------------------------------------------------------------------------- 1 | export function escape( 2 | escapes: [RegExp, string | ((substring: string, ...args: any[]) => string)][], 3 | string: string 4 | ) { 5 | return escapes.reduce(function(accumulator, escape) { 6 | return accumulator.replace(escape[0], escape[1]); 7 | }, string); 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/src/plugins/del.ts: -------------------------------------------------------------------------------- 1 | // del 2 | import Service from '../service'; 3 | 4 | export const applyDelRule = (service: Service) => { 5 | service.addRule('del', { 6 | filter: ['del', 's'], 7 | 8 | replacement: function(content) { 9 | return '~~' + content + '~~'; 10 | }, 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/core/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | 'ts-jest': { 4 | diagnostics: { 5 | pathRegex: /\.(spec|test)\.ts$/ 6 | } 7 | } 8 | }, 9 | "transform": { 10 | "\\.md$": "jest-raw-loader", 11 | "\\.html": "jest-raw-loader", 12 | '.(ts|tsx|js|jsx)': 'ts-jest', 13 | } 14 | } -------------------------------------------------------------------------------- /packages/core/src/util/isFence.ts: -------------------------------------------------------------------------------- 1 | import { Options, Node } from '../types'; 2 | 3 | export function isFence(options: Options, node: Node): boolean { 4 | return !!( 5 | options.codeBlockStyle === 'fenced' && 6 | node.nodeName === 'PRE' && 7 | node.firstChild && 8 | node.firstChild.nodeName === 'CODE' 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /packages/@sitdown/wechat/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | 'ts-jest': { 4 | diagnostics: { 5 | pathRegex: /\.(spec|test)\.ts$/ 6 | } 7 | } 8 | }, 9 | "transform": { 10 | "\\.md$": "jest-raw-loader", 11 | "\\.html": "jest-raw-loader", 12 | '.(ts|tsx|js|jsx)': 'ts-jest', 13 | } 14 | } -------------------------------------------------------------------------------- /packages/@sitdown/zhihu/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | 'ts-jest': { 4 | diagnostics: { 5 | pathRegex: /\.(spec|test)\.ts$/ 6 | } 7 | } 8 | }, 9 | "transform": { 10 | "\\.md$": "jest-raw-loader", 11 | "\\.html": "jest-raw-loader", 12 | '.(ts|tsx|js|jsx)': 'ts-jest', 13 | } 14 | } -------------------------------------------------------------------------------- /packages/@sitdown/juejin/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | 'ts-jest': { 4 | diagnostics: { 5 | pathRegex: /\.(spec|test)\.ts$/ 6 | } 7 | } 8 | }, 9 | "transform": { 10 | "\\.md$": "jest-raw-loader", 11 | "\\.html": "jest-raw-loader", 12 | '.(ts|tsx|js|jsx)': 'ts-jest', 13 | } 14 | }; -------------------------------------------------------------------------------- /packages/core/src/util/isCode.ts: -------------------------------------------------------------------------------- 1 | import { Node } from '../types'; 2 | 3 | export function isCode(node: Node) { 4 | var hasSiblings = node.previousSibling || node.nextSibling; 5 | var isCodeBlock = 6 | node.parentNode && node.parentNode.nodeName === 'PRE' && !hasSiblings; 7 | 8 | return node.nodeName === 'CODE' && !isCodeBlock; 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/test/spec/markdownPaper/table-no-header.md: -------------------------------------------------------------------------------- 1 | | | 2 | | --- | 3 | |
[TypeScript 3.9 Beta Released](https://javascriptweekly.com/link/86403/web "devblogs.microsoft.com") — 3.9’s focus is on “performance, polish, and stability” with the most significant change you’re likely to notice being faster compile times.
Daniel Rosenwasser \(Microsoft\)
| -------------------------------------------------------------------------------- /packages/@sitdown/javascriptweekly/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | 'ts-jest': { 4 | diagnostics: { 5 | pathRegex: /\.(spec|test)\.ts$/ 6 | } 7 | } 8 | }, 9 | "transform": { 10 | "\\.md$": "jest-raw-loader", 11 | "\\.html": "jest-raw-loader", 12 | '.(ts|tsx|js|jsx)': 'ts-jest', 13 | } 14 | }; -------------------------------------------------------------------------------- /packages/core/src/util/indentCodeIsListfirstChild.ts: -------------------------------------------------------------------------------- 1 | import { Options, Node } from '../types'; 2 | 3 | export function IndentCodeIsListfirstChild(list: Node, options: Options) { 4 | return ( 5 | options.codeBlockStyle !== 'fenced' && 6 | list && 7 | list.firstChild && 8 | list.nodeName === 'LI' && 9 | list.firstChild.nodeName === 'PRE' 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/plugins/fencedCodeBlock.ts: -------------------------------------------------------------------------------- 1 | import Service from '../service'; 2 | import { fenceReplacement, isFence } from '../util'; 3 | 4 | export const applyFenceRule = (service: Service) => { 5 | service.addRule('fencedCodeBlock', { 6 | filter: function(node, options) { 7 | return isFence(options, node); 8 | }, 9 | 10 | replacement: fenceReplacement, 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/core/src/plugins/strong.ts: -------------------------------------------------------------------------------- 1 | import Service from '../service'; 2 | 3 | export const applyStrongRule = (service: Service) => { 4 | service.addRule('hr', { 5 | filter: ['strong', 'b'], 6 | 7 | replacement: function(content, _node, options) { 8 | if (!content.trim()) return ''; 9 | return options.strongDelimiter + content + options.strongDelimiter; 10 | }, 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/core/src/util/findParentNumber.ts: -------------------------------------------------------------------------------- 1 | import { Node } from '../types'; 2 | 3 | export function findParentNumber( 4 | node: Node, 5 | parentName: string, 6 | count = 0 7 | ): number { 8 | if (!node.parentNode) { 9 | return count; 10 | } 11 | if (node.parentNode.nodeName === parentName) { 12 | count++; 13 | } 14 | 15 | return findParentNumber(node.parentNode as HTMLElement, parentName, count); 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/test/table-no-header.test.ts: -------------------------------------------------------------------------------- 1 | import { Sitdown } from '../src'; 2 | import md from './spec/markdownPaper/table-no-header.md'; 3 | import html from './spec/markdownPaper/table-no-header.html'; 4 | 5 | describe('table no header', () => { 6 | it('works', () => { 7 | let sitdown = new Sitdown({ 8 | convertNoHeaderTable:true 9 | }); 10 | const expected = sitdown.HTMLToMD(html); 11 | expect(expected).toEqual(md); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/@sitdown/wechat/test/spec/markdownPaper/formula.md: -------------------------------------------------------------------------------- 1 | $\ce{Hg^2+ ->[I-] HgI2 ->[I-] [Hg^{II}I4]^2-}$ 2 | 3 | $$ 4 | H(D_2) = -\left(\frac{2}{4}\log_2 \frac{2}{4} + \frac{2}{4}\log_2 \frac{2}{4}\right) = 1 5 | $$ 6 | 7 | 矩阵: 8 | 9 | $$ 10 | \begin{pmatrix} 11 | 1 & a_1 & a_1^2 & \cdots & a_1^n \\ 12 | 1 & a_2 & a_2^2 & \cdots & a_2^n \\ 13 | \vdots & \vdots & \vdots & \ddots & \vdots \\ 14 | 1 & a_m & a_m^2 & \cdots & a_m^n \\ 15 | \end{pmatrix} 16 | $$ -------------------------------------------------------------------------------- /packages/core/src/util/isKeep.ts: -------------------------------------------------------------------------------- 1 | import { Options, Node } from '../types'; 2 | 3 | export function isKeep(options: Options, node: Node): boolean { 4 | const filters = options.keepFilter ? options.keepFilter : ['div', 'style']; 5 | 6 | return Array.isArray(filters) 7 | ? filters.some(filter => filter === node.nodeName.toLowerCase()) 8 | : typeof filters === 'function' 9 | ? filters(node, options) 10 | : filters === node.nodeName.toLowerCase(); 11 | } 12 | -------------------------------------------------------------------------------- /packages/@sitdown/javascriptweekly/README.md: -------------------------------------------------------------------------------- 1 | # `@sitdown/javascriptweekly` 2 | 3 | javascriptweekly html 转 md 注意处: 4 | 1. 表格布局 5 | 6 | ## Usage 7 | 8 | ``` 9 | import { Sitdown } from 'sitdown'; 10 | import { applyJSWeeklyRule } from '@sitdown/javascriptweekly'; 11 | 12 | let sitdown = new Sitdown({ 13 | keepFilter: ['style'], 14 | codeBlockStyle: 'fenced', 15 | bulletListMarker: '-', 16 | hr: '---', 17 | }); 18 | sitdown.use(applyJSWeeklyRule); 19 | ``` 20 | -------------------------------------------------------------------------------- /packages/core/src/util/isVoid.ts: -------------------------------------------------------------------------------- 1 | import { Node } from '../types'; 2 | 3 | export const voidElements = [ 4 | 'area', 5 | 'base', 6 | 'br', 7 | 'col', 8 | 'command', 9 | 'embed', 10 | 'hr', 11 | 'img', 12 | 'input', 13 | 'keygen', 14 | 'link', 15 | 'meta', 16 | 'param', 17 | 'source', 18 | 'track', 19 | 'wbr', 20 | ]; 21 | 22 | export default function isVoid(node: Node) { 23 | return voidElements.indexOf(node.nodeName.toLowerCase()) !== -1; 24 | } 25 | -------------------------------------------------------------------------------- /packages/@sitdown/juejin/README.md: -------------------------------------------------------------------------------- 1 | # `@sitdown/juejin` 2 | 3 | 掘金 html 转 md 注意处: 4 | 1. 掘金进行了中文排版美化,如数字与中文、中文与英文间有空格 5 | 2. 掘金对图片进行了转储,地址存储在 img 标签的 data-src 下 6 | 3. 掘金对代码块做了处理,多了一个复制代码 7 | 8 | ## Usage 9 | 10 | ``` 11 | import { Sitdown } from 'sitdown'; 12 | import { applyJuejinRule } from '@sitdown/juejin'; 13 | 14 | let sitdown = new Sitdown({ 15 | keepFilter: ['style'], 16 | codeBlockStyle: 'fenced', 17 | bulletListMarker: '-', 18 | hr: '---', 19 | }); 20 | sitdown.use(applyJuejinRule); 21 | ``` 22 | -------------------------------------------------------------------------------- /packages/core/src/util/index.ts: -------------------------------------------------------------------------------- 1 | export { escape } from './escape'; 2 | export { isFence } from './isFence'; 3 | export { isCode } from './isCode'; 4 | export { 5 | blankReplacement, 6 | listReplacement, 7 | fenceReplacement, 8 | keepReplacement, 9 | } from './replacement'; 10 | export { repeat } from './repeat'; 11 | export { findParentNumber } from './findParentNumber'; 12 | export { findOrderListIndentNumber } from './findOrderListIndentNumber'; 13 | export { IndentCodeIsListfirstChild } from './indentCodeIsListfirstChild'; 14 | -------------------------------------------------------------------------------- /packages/@sitdown/zhihu/README.md: -------------------------------------------------------------------------------- 1 | # `@sitdown/zhihu` 2 | 3 | 知乎 html 转 md 注意处: 4 | 1. 知乎进行了中文排版美化,如数字与中文、中文与英文间有空格 5 | 2. 图片转储了,并在 noscript 里有一份备份 6 | 3. 图片、链接描述(alt)没了 7 | 4. 知乎将公式转成图片了 8 | 5. 知乎把强调的链接的强调给滤掉了 9 | 10 | ## Usage 11 | 12 | ``` 13 | import { Sitdown } from 'sitdown'; 14 | import { applyZhihuRule } from '@sitdown/zhihu'; 15 | let sitdown = new Sitdown({ 16 | keepFilter: ['style'], 17 | codeBlockStyle: 'fenced', 18 | bulletListMarker: '-', 19 | hr: '---', 20 | }); 21 | sitdown.use(applyZhihuRule); 22 | 23 | ``` 24 | -------------------------------------------------------------------------------- /packages/core/src/plugins/image.ts: -------------------------------------------------------------------------------- 1 | import Service from '../service'; 2 | 3 | export const applyImageRule = (service: Service) => { 4 | service.addRule('hr', { 5 | filter: 'img', 6 | 7 | replacement: function(_content, node) { 8 | var alt = node.getAttribute('alt') || ''; 9 | var src = node.getAttribute('src') || ''; 10 | var title = node.title || ''; 11 | var titlePart = title ? ' "' + title + '"' : ''; 12 | return src ? '![' + alt + ']' + '(' + src + titlePart + ')' : ''; 13 | }, 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/core/src/plugins/taskListItems.ts: -------------------------------------------------------------------------------- 1 | import Service from '../service'; 2 | 3 | export const applyTaskRule = (service: Service) => { 4 | service.addRule('task', { 5 | filter: function(node) { 6 | return ( 7 | (node as HTMLInputElement).type === 'checkbox' && 8 | node.parentNode != null && 9 | node.parentNode.nodeName === 'LI' 10 | ); 11 | }, 12 | replacement: function(_content, node) { 13 | return ((node as HTMLInputElement).checked ? '[x]' : '[ ]') + ' '; 14 | }, 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/core/src/util/replacement/keep.ts: -------------------------------------------------------------------------------- 1 | import { Node } from '../../types'; 2 | 3 | export function keepReplacement(content: string, node: Node) { 4 | let html = (node as HTMLElement).outerHTML; 5 | if (!content) { 6 | let attrs = ''; 7 | for (let i = 0; i < node.attributes.length; i++) { 8 | let attr = node.attributes[i]; 9 | attrs += `${attr.name}="${attr.nodeValue}"`; 10 | } 11 | html = `<${node.localName.toLowerCase()} ${attrs} />`; 12 | } 13 | return node.isBlock ? '\n\n' + html + '\n' : html; 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/plugins/blockquote.ts: -------------------------------------------------------------------------------- 1 | import Service from '../service'; 2 | 3 | export const applyBlockquoteRule = (service: Service) => { 4 | service.addRule('hr', { 5 | filter: 'blockquote', 6 | 7 | replacement: function(content, node) { 8 | var parent = node.parentNode; 9 | var parentIsList = parent && parent.nodeName === 'LI'; 10 | var blank = parentIsList ? '\n' : '\n\n'; 11 | content = content.replace(/^\n+|\n+$/g, '').replace(/^/gm, '> '); 12 | return blank + content + blank; 13 | }, 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | 8 | 9 | 10 | ## Feature request 11 | 12 | #### What problem does this feature solve? 13 | 14 | #### What does the proposed API look like? 15 | 16 | #### How should this be implemented in your opinion? 17 | 18 | #### Are you willing to work on this yourself? 19 | -------------------------------------------------------------------------------- /packages/core/src/util/findOrderListIndentNumber.ts: -------------------------------------------------------------------------------- 1 | import { Node } from '../types'; 2 | 3 | export function findOrderListIndentNumber(node: Node, count = 0): number { 4 | const parentName = 'OL'; 5 | const parent = node.parentNode as HTMLElement; 6 | if (!parent) { 7 | return count; 8 | } 9 | if (parent.nodeName === parentName) { 10 | var start = parent.getAttribute('start'); 11 | if (start && start.length > 1) { 12 | count += start.length - 1; 13 | } 14 | } 15 | 16 | return findOrderListIndentNumber(parent, count); 17 | } 18 | -------------------------------------------------------------------------------- /packages/@sitdown/javascriptweekly/test/spec/markdownPaper/paper1.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [ECMAScript 2020: The Final Feature Set](https://javascriptweekly.com/link/86398/web "2ality.com") — TC39 has just approved the [ECMAScript 2020 spec](https://javascriptweekly.com/link/86399/web) _\(a full weekend's bedtime reading right there\!\)_ with Ecma GA approval due in a few months, but what’s new\? Dr. Axel rounds it up with links to the included stage 4 proposals. If you prefer something more code-driven, Pawel Grzybek has [a similar roundup.](https://javascriptweekly.com/link/86426/web) 4 | 5 | Dr. Axel Rauschmayer -------------------------------------------------------------------------------- /packages/docs/.vuepress/enhanceApp.js: -------------------------------------------------------------------------------- 1 | import Pagination from 'ant-design-vue/lib/pagination'; // 加载 JS 2 | import Radio from 'ant-design-vue/lib/radio'; // 加载 JS 3 | import 'ant-design-vue/lib/radio/style/css'; // 加载 CSS 4 | import 'ant-design-vue/lib/pagination/style/css'; // 加载 CSS 5 | export default ({ 6 | Vue, // 当前 VuePress 应用所使用的 Vue 版本 7 | options, // 根 Vue 实例的选项 8 | router, // 应用程序的路由实例 9 | siteData // 网站元数据 10 | }) => { 11 | // ...使用应用程序级别的增强功能 12 | Vue.use(Pagination); 13 | Vue.use(Radio); 14 | } -------------------------------------------------------------------------------- /packages/core/test/csdn.test.ts: -------------------------------------------------------------------------------- 1 | import { Sitdown } from '../src'; 2 | import md from './spec/markdownPaper/paper2-csdn.md'; 3 | import html from './spec/markdownPaper/paper2-csdn.html'; 4 | 5 | /* 6 | csdn html 转 md 存在的不一致处: 7 | 1. csdn 开头有多余的注释 8 | 2. csdn 的标题带了很多空链接 9 | */ 10 | describe('csdn', () => { 11 | it('works', () => { 12 | let sitdown = new Sitdown({ 13 | keepFilter: ['style'], 14 | codeBlockStyle: 'fenced', 15 | bulletListMarker: '-', 16 | hr: '---', 17 | }); 18 | const expected = sitdown.HTMLToMD(html); 19 | expect(expected).toEqual(md); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/@sitdown/javascriptweekly/tsdx.config.js: -------------------------------------------------------------------------------- 1 | // Not transpiled with TypeScript or Babel, so use plain Es6/Node.js! 2 | const plugin = { 3 | name: 'replace', 4 | renderChunk(code) { 5 | return code 6 | .replace(/sitdown\/dist\/src.esm/g, 'sitdown/dist/src.cjs.development'); 7 | }, 8 | } 9 | module.exports = { 10 | // This function will run for each entry/format/env combination 11 | rollup(config, options) { 12 | if (options.format === 'cjs') { 13 | config.plugins.push(plugin); 14 | } 15 | return config; // always return a config. 16 | }, 17 | }; -------------------------------------------------------------------------------- /packages/@sitdown/javascriptweekly/test/javascriptweekly.test.ts: -------------------------------------------------------------------------------- 1 | import { Sitdown } from 'sitdown'; 2 | import { applyJSWeeklyRule } from '../src'; 3 | 4 | describe('javascriptweekly', () => { 5 | it('works', () => { 6 | let sitdown = new Sitdown({ 7 | keepFilter: ['style'], 8 | codeBlockStyle: 'fenced', 9 | bulletListMarker: '-', 10 | hr: '---', 11 | convertNoHeaderTable: true, 12 | }); 13 | sitdown.use(applyJSWeeklyRule); 14 | const expected = sitdown.HTMLToMD( 15 | require('./spec/markdownPaper/paper1.html') 16 | ); 17 | expect(expected).toEqual(require('./spec/markdownPaper/paper1.md')); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/core/src/plugins/em.ts: -------------------------------------------------------------------------------- 1 | //em 2 | import Service from '../service'; 3 | 4 | export const applyEmRule = (service: Service) => { 5 | service.addRule('em', { 6 | filter: ['em', 'i'], 7 | 8 | replacement: function(content, node, options) { 9 | if (!content.trim()) return ''; 10 | let emDelimiter = options.emDelimiter; 11 | if ( 12 | node.parentNode && 13 | node.parentNode.nodeName === 'EM' && 14 | node.parentNode.firstChild === node.parentNode.lastChild 15 | ) { 16 | emDelimiter = emDelimiter === '_' ? '*' : '_'; 17 | } 18 | return emDelimiter + content + emDelimiter; 19 | }, 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/core/src/plugins/paragraph.ts: -------------------------------------------------------------------------------- 1 | import Service from '../service'; 2 | 3 | var escapes: [RegExp, string][] = [[/\s-/g, ' \\-']]; 4 | 5 | function escape(string: string) { 6 | return escapes.reduce(function(accumulator, escape) { 7 | return accumulator.replace(escape[0], escape[1]); 8 | }, string); 9 | } 10 | export const applyParagraphRule = (service: Service) => { 11 | service.addRule('paragraph', { 12 | filter: 'p', 13 | 14 | replacement: function(content, node) { 15 | const hasCommentChild = Array.from(node.childNodes).some( 16 | item => item.nodeType === 8 17 | ); 18 | return '\n\n' + (hasCommentChild ? content : escape(content)) + '\n\n'; 19 | }, 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/core/src/service/Node/isBlank.ts: -------------------------------------------------------------------------------- 1 | import isVoid, { voidElements } from '../../util/isVoid'; 2 | import { Node } from '../../types'; 3 | 4 | var voidSelector = voidElements.join(); 5 | function hasVoid(node: Node) { 6 | return node.querySelector && node.querySelector(voidSelector); 7 | } 8 | export default function isBlank(node: Node) { 9 | return ( 10 | [ 11 | 'A', 12 | 'TABLE', 13 | 'THEAD', 14 | 'TBODY', 15 | 'TR', 16 | 'TH', 17 | 'TD', 18 | 'IFRAME', 19 | 'SCRIPT', 20 | 'AUDIO', 21 | 'VIDEO', 22 | ].indexOf(node.nodeName) === -1 && 23 | /^\s*$/i.test(node.textContent || '') && 24 | !isVoid(node) && 25 | !hasVoid(node) 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/plugins/list.ts: -------------------------------------------------------------------------------- 1 | // list 2 | import Service from '../service'; 3 | import { listReplacement } from '../util'; 4 | 5 | export const applyListRule = (service: Service) => { 6 | service.addRule('list', { 7 | filter: ['ul', 'ol'], 8 | 9 | replacement: function(content, node) { 10 | var parent = node.parentNode; 11 | if ( 12 | parent && 13 | parent.nodeName === 'LI' && 14 | parent.lastElementChild === node 15 | ) { 16 | return '\n' + content; 17 | } else { 18 | return '\n\n' + content + '\n\n'; 19 | } 20 | }, 21 | }); 22 | 23 | service.addRule('listItem', { 24 | filter: 'li', 25 | 26 | replacement: listReplacement, 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/@sitdown/juejin/test/juejin.test.ts: -------------------------------------------------------------------------------- 1 | import { Sitdown } from 'sitdown'; 2 | import { applyJuejinRule } from '../src'; 3 | import md from './spec/markdownPaper/paper1-juejin.md'; 4 | import html from './spec/markdownPaper/paper1-juejin.html'; 5 | 6 | /* 7 | 掘金 html 转 md 存在的不一致处: 8 | 1. 掘金进行了中文排版美化,如数字与中文、中文与英文间有空格 9 | 2. 掘金对图片进行了转储,地址存储在 img 标签的 data-src 下 10 | 3. 掘金对代码块做了处理,多了一个复制代码 11 | */ 12 | describe('juejin', () => { 13 | it('works', () => { 14 | let sitdown = new Sitdown({ 15 | keepFilter: ['style'], 16 | codeBlockStyle: 'fenced', 17 | bulletListMarker: '-', 18 | hr: '---', 19 | }); 20 | sitdown.use(applyJuejinRule); 21 | const expected = sitdown.HTMLToMD(html); 22 | expect(expected).toEqual(md); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/@sitdown/zhihu/src/p.ts: -------------------------------------------------------------------------------- 1 | import Service from 'sitdown/dist/service'; 2 | 3 | var escapes: [RegExp, string][] = [[/\s-/g, ' \\-']]; 4 | 5 | function escape(string: string) { 6 | return escapes.reduce(function(accumulator, escape) { 7 | return accumulator.replace(escape[0], escape[1]); 8 | }, string); 9 | } 10 | export const applyParagraphRule = (service: Service) => { 11 | service.addRule('paragraph', { 12 | filter: 'p', 13 | 14 | replacement: function(content, node) { 15 | const cantEscape = Array.from(node.childNodes).some( 16 | (item: ChildNode & { isFormula?: boolean }) => 17 | item.nodeType === 8 || item.isFormula 18 | ); 19 | return '\n\n' + (cantEscape ? content : escape(content)) + '\n\n'; 20 | }, 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: remote ssh command 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | build: 8 | name: Build 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: executing remote ssh commands using password 12 | uses: appleboy/ssh-action@master 13 | with: 14 | host: ${{ secrets.HOST }} 15 | username: ${{ secrets.USERNAME }} 16 | password: ${{ secrets.PASSWORD }} 17 | port: ${{ secrets.PORT }} 18 | command_timeout: 5m 19 | script: | 20 | cd /home/mdnice/sitdown 21 | git stash 22 | git pull origin master 23 | cd packages/docs 24 | rm -rf node_modules/ 25 | rm -rf package-lock.json 26 | cnpm install 27 | sudo yarn run build -------------------------------------------------------------------------------- /packages/@sitdown/juejin/src/index.ts: -------------------------------------------------------------------------------- 1 | import Service from 'sitdown/dist/service'; 2 | 3 | export const applyJuejinRule = (service: Service) => { 4 | service.addRule('juejinImg', { 5 | filter: 'img', 6 | 7 | replacement: function(_content: string, node) { 8 | var alt = node.getAttribute('alt') || ''; 9 | var src = node.getAttribute('data-src') || node.getAttribute('src') || ''; 10 | var title = node.title || ''; 11 | var titlePart = title ? ' "' + title + '"' : ''; 12 | return src ? '![' + alt + ']' + '(' + src + titlePart + ')' : ''; 13 | }, 14 | }); 15 | 16 | service.addRule('juejinCopyCode', { 17 | filter(node) { 18 | return (node.tagName === 'SPAN' && 19 | node.innerText === '复制代码') 20 | }, 21 | replacement() { 22 | return '' 23 | } 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /packages/core/src/plugins/heading.ts: -------------------------------------------------------------------------------- 1 | import Service from '../service'; 2 | import { repeat, escape } from '../util'; 3 | 4 | var escapes: [RegExp, string][] = [[/\s#/g, ' \\#']]; 5 | 6 | export const applyHeadingRule = (service: Service) => { 7 | service.addRule('heading', { 8 | filter: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], 9 | 10 | replacement: function(content, node, options) { 11 | var hLevel = Number(node.nodeName.charAt(1)); 12 | 13 | if (options.headingStyle === 'setext' && hLevel < 3) { 14 | var underline = repeat(hLevel === 1 ? '=' : '-', content.length); 15 | return '\n\n' + content + '\n' + underline + '\n\n'; 16 | } else { 17 | return ( 18 | '\n\n' + repeat('#', hLevel) + ' ' + escape(escapes, content) + '\n\n' 19 | ); 20 | } 21 | }, 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/core/src/service/Node/index.ts: -------------------------------------------------------------------------------- 1 | import isBlock from '../../util/isBlock'; 2 | import isBlank from './isBlank'; 3 | import flankingWhitespace from './flankingWhitespace'; 4 | 5 | export default class Node { 6 | unNeedEscape?: boolean; 7 | data?: string; 8 | isBlank?: boolean; 9 | isBlock?: boolean; 10 | isCode?: boolean; 11 | flankingWhitespace?: { 12 | leading: string; 13 | trailing: string; 14 | }; 15 | 16 | constructor(node: HTMLElement) { 17 | const newNode = node as Node & HTMLElement; 18 | newNode.isBlock = isBlock(node); 19 | newNode.isCode = 20 | node.nodeName.toLowerCase() === 'code' || 21 | (node.parentNode as Node).isCode; 22 | newNode.isBlank = isBlank(node); 23 | newNode.flankingWhitespace = flankingWhitespace(node); 24 | return Object.assign(node, newNode); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/src/util/replacement/list/index.ts: -------------------------------------------------------------------------------- 1 | import { Options, Node } from '../../../types'; 2 | import { ListNode } from './listNode'; 3 | 4 | export function listReplacement(content: string, node: Node, options: Options) { 5 | var listNode = new ListNode(node); 6 | var { isLoose, isNewList: newList } = listNode; 7 | 8 | var bulletListMarker = newList ? '+' : options.bulletListMarker; 9 | var prefix = listNode.caclPrefix(bulletListMarker + ' '); 10 | 11 | content = content 12 | .replace(/^\n+/, '') // remove leading newlines 13 | .replace(/\n+$/, '\n') // replace trailing newlines with just a single one 14 | .replace(/\n(\S)/gm, listNode.lineIndent(options)); // indent 15 | 16 | return ( 17 | prefix + 18 | content + 19 | (node.nextSibling && !/\n$/.test(content) ? '\n' : '') + 20 | (isLoose ? '\n' : '') 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/src/service/Rules/findRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule, Node, TagName, Options } from '../../types'; 2 | 3 | function filterValue(rule: Rule, node: Node, options: Options) { 4 | var filter = rule.filter; 5 | if (typeof filter === 'string') { 6 | return filter === node.nodeName.toLowerCase(); 7 | } else if (Array.isArray(filter)) { 8 | return filter.indexOf(node.nodeName.toLowerCase()) > -1; 9 | } else if (typeof filter === 'function') { 10 | return filter.call(rule, node, options); 11 | } else { 12 | throw new TypeError('`filter` needs to be a string, array, or function'); 13 | } 14 | } 15 | 16 | export default function findRule(rules: Rule[], node: Node, options: Options) { 17 | for (var i = 0; i < rules.length; i++) { 18 | var rule = rules[i]; 19 | if (filterValue(rule, node, options)) return rule; 20 | } 21 | return void 0; 22 | } 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | - [ ] I confirm that this is an issue rather than a question. 15 | 16 | ## Bug report 17 | 18 | #### Steps to reproduce 19 | 20 | 21 | 22 | #### What is expected? 23 | 24 | #### What is actually happening? 25 | 26 | #### Other relevant information 27 | 28 | -------------------------------------------------------------------------------- /packages/@sitdown/wechat/README.md: -------------------------------------------------------------------------------- 1 | # `@sitdown/wechat` 2 | 3 | 微信 html 转 md 注意处: 4 | 1. 图片转储,且访问有鉴权 5 | 2. 图片描述放在 figcaption 里 6 | 3. 微信不支持外链,所以用脚注的方式兼容。 7 | 4. 代码块转回来永远是一行。 8 | 5. 微信有独创的居中块,html 是 span 带有 `display:block;text-align:center;` 样式 9 | 6. 公式用 svg 画出来的 10 | 7. 文章开头的作者信息和结束的信息,包裹在 section 里 11 | 12 | ## Usage 13 | 14 | ``` 15 | import { Sitdown,RootNode } from 'sitdown'; 16 | import { applyWechatRule, extraFootLinks } from '@sitdown/wechat'; 17 | 18 | let sitdown = new Sitdown({ 19 | keepFilter: ['style'], 20 | codeBlockStyle: 'fenced', 21 | bulletListMarker: '-', 22 | hr: '---', 23 | }); 24 | sitdown.use(applyWechatRule); 25 | 26 | ``` 27 | 28 | support mdnice wechat footlink: 29 | ```ts 30 | const wechatToMD = (html: string) => { 31 | const root = new sitdown.RootNode(html); 32 | const footLinks = extraFootLinks(root); 33 | return sitdown.HTMLToMD(html, { footLinks }); 34 | }; 35 | ``` 36 | -------------------------------------------------------------------------------- /packages/docs/.vuepress/components/Examples.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 27 | 28 | -------------------------------------------------------------------------------- /.github/workflows/dev.yml: -------------------------------------------------------------------------------- 1 | name: validate npm build script 2 | on: 3 | push: 4 | branches: 5 | - dev 6 | - master 7 | - main 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | steps: 13 | # action命令,切换分支获取源码 14 | - name: Checkout 15 | # 使用action库 actions/checkout获取源码 16 | uses: actions/checkout@master 17 | # action命令,安装Node10 18 | - name: use Node.js 10 19 | # 使用action库 actions/setup-node安装node 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: 10 23 | # action命令,install && build 24 | - name: bootstrap and build 25 | # 运行的命令或者 action 26 | run: | 27 | npm install lerna@3.20.2 -g 28 | lerna bootstrap --registry http://r.cnpmjs.org/ --ignore-scripts 29 | npm run build 30 | npm run test 31 | # 环境变量 32 | env: 33 | CI: true 34 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src", "types", "test"], 4 | "compilerOptions": { 5 | "target": "es5", 6 | "module": "esnext", 7 | "lib": ["dom", "esnext"], 8 | "importHelpers": true, 9 | "declaration": true, 10 | "sourceMap": true, 11 | "rootDir": "./", 12 | "strict": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "strictFunctionTypes": true, 16 | "strictPropertyInitialization": true, 17 | "noImplicitThis": true, 18 | "alwaysStrict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noImplicitReturns": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "moduleResolution": "node", 24 | "baseUrl": "./", 25 | "paths": { 26 | "*": ["src/*", "node_modules/*","lib/*"] 27 | }, 28 | "jsx": "react", 29 | "esModuleInterop": true, 30 | "resolveJsonModule": true, 31 | "allowJs": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/core/src/service/HTMLParser.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Set up window for Node.js 3 | */ 4 | 5 | var root = typeof window !== 'undefined' ? window : {}; 6 | 7 | /* 8 | * Parsing HTML strings 9 | */ 10 | 11 | function canParseHTMLNatively() { 12 | var Parser = (root as Window & typeof globalThis).DOMParser; 13 | var canParse = false; 14 | 15 | // Adapted from https://gist.github.com/1129031 16 | // Firefox/Opera/IE throw errors on unsupported types 17 | try { 18 | // WebKit returns null on unsupported types 19 | if (new Parser().parseFromString('', 'text/html')) { 20 | canParse = true; 21 | } 22 | } catch (e) {} 23 | 24 | return canParse; 25 | } 26 | 27 | class Parser { 28 | parseFromString(string: string) { 29 | const JSDOM = require('jsdom').JSDOM; 30 | return new JSDOM(string).window.document; 31 | } 32 | } 33 | 34 | export default canParseHTMLNatively() 35 | ? (root as Window & typeof globalThis).DOMParser 36 | : Parser; 37 | -------------------------------------------------------------------------------- /packages/@sitdown/zhihu/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src", "types", "test"], 4 | "compilerOptions": { 5 | "target": "es5", 6 | "module": "esnext", 7 | "lib": ["dom", "esnext"], 8 | "importHelpers": true, 9 | "declaration": true, 10 | "sourceMap": true, 11 | "rootDir": "./", 12 | "strict": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "strictFunctionTypes": true, 16 | "strictPropertyInitialization": true, 17 | "noImplicitThis": true, 18 | "alwaysStrict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noImplicitReturns": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "moduleResolution": "node", 24 | "baseUrl": "./", 25 | "paths": { 26 | "*": ["src/*", "node_modules/*","lib/*"] 27 | }, 28 | "jsx": "react", 29 | "esModuleInterop": true, 30 | "resolveJsonModule": true, 31 | "allowJs": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/core/src/util/isBlock.ts: -------------------------------------------------------------------------------- 1 | import { Node } from '../types'; 2 | 3 | var blockElements = [ 4 | 'address', 5 | 'article', 6 | 'aside', 7 | 'audio', 8 | 'blockquote', 9 | 'body', 10 | 'canvas', 11 | 'center', 12 | 'dd', 13 | 'dir', 14 | 'div', 15 | 'dl', 16 | 'dt', 17 | 'fieldset', 18 | 'figcaption', 19 | 'figure', 20 | 'footer', 21 | 'form', 22 | 'frameset', 23 | 'h1', 24 | 'h2', 25 | 'h3', 26 | 'h4', 27 | 'h5', 28 | 'h6', 29 | 'header', 30 | 'hgroup', 31 | 'hr', 32 | 'html', 33 | 'isindex', 34 | 'li', 35 | 'main', 36 | 'menu', 37 | 'nav', 38 | 'noframes', 39 | 'noscript', 40 | 'ol', 41 | 'output', 42 | 'p', 43 | 'pre', 44 | 'section', 45 | 'table', 46 | 'tbody', 47 | 'td', 48 | 'tfoot', 49 | 'th', 50 | 'thead', 51 | 'tr', 52 | 'ul', 53 | ]; 54 | 55 | export default function isBlock(node: Node) { 56 | return blockElements.indexOf(node.nodeName.toLowerCase()) !== -1; 57 | } 58 | -------------------------------------------------------------------------------- /packages/@sitdown/juejin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src", "types", "test"], 4 | "compilerOptions": { 5 | "target": "es5", 6 | "module": "esnext", 7 | "lib": ["dom", "esnext"], 8 | "importHelpers": true, 9 | "declaration": true, 10 | "sourceMap": true, 11 | "rootDir": "./", 12 | "strict": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "strictFunctionTypes": true, 16 | "strictPropertyInitialization": true, 17 | "noImplicitThis": true, 18 | "alwaysStrict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noImplicitReturns": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "moduleResolution": "node", 24 | "baseUrl": "./", 25 | "paths": { 26 | "*": ["src/*", "node_modules/*","lib/*"] 27 | }, 28 | "jsx": "react", 29 | "esModuleInterop": true, 30 | "resolveJsonModule": true, 31 | "allowJs": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/@sitdown/wechat/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src", "types", "test"], 4 | "compilerOptions": { 5 | "target": "es5", 6 | "module": "esnext", 7 | "lib": ["dom", "esnext"], 8 | "importHelpers": true, 9 | "declaration": true, 10 | "sourceMap": true, 11 | "rootDir": "./", 12 | "strict": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "strictFunctionTypes": true, 16 | "strictPropertyInitialization": true, 17 | "noImplicitThis": true, 18 | "alwaysStrict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noImplicitReturns": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "moduleResolution": "node", 24 | "baseUrl": "./", 25 | "paths": { 26 | "*": ["src/*", "node_modules/*","lib/*"] 27 | }, 28 | "jsx": "react", 29 | "esModuleInterop": true, 30 | "resolveJsonModule": true, 31 | "allowJs": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/@sitdown/javascriptweekly/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src", "types", "test"], 4 | "compilerOptions": { 5 | "target": "es5", 6 | "module": "esnext", 7 | "lib": ["dom", "esnext"], 8 | "importHelpers": true, 9 | "declaration": true, 10 | "sourceMap": true, 11 | "rootDir": "./", 12 | "strict": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "strictFunctionTypes": true, 16 | "strictPropertyInitialization": true, 17 | "noImplicitThis": true, 18 | "alwaysStrict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noImplicitReturns": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "moduleResolution": "node", 24 | "baseUrl": "./", 25 | "paths": { 26 | "*": ["src/*", "node_modules/*","lib/*"] 27 | }, 28 | "jsx": "react", 29 | "esModuleInterop": true, 30 | "resolveJsonModule": true, 31 | "allowJs": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import applyPlugins from './plugins'; 2 | import { blankReplacement, keepReplacement } from './util'; 3 | import Service from './service'; 4 | import * as Util from './util'; 5 | import { Options } from './types'; 6 | 7 | export class Sitdown { 8 | defaultOptions: Options; 9 | service: Service; 10 | 11 | constructor(options?: Options) { 12 | this.defaultOptions = { 13 | headingStyle: 'atx', 14 | blankReplacement, 15 | keepReplacement, 16 | }; 17 | this.service = new Service({ 18 | ...this.defaultOptions, 19 | ...options, 20 | }); 21 | applyPlugins(this.service); 22 | } 23 | 24 | HTMLToMD(html: string, env?: object) { 25 | if (env) { 26 | this.service.options.env = env; 27 | } 28 | return this.service.turndown(html); 29 | } 30 | 31 | use(plugin: Plugin | Plugin[]) { 32 | this.service.use(plugin); 33 | return this; 34 | } 35 | } 36 | export type Plugin = (service: Service) => void; 37 | export { default as RootNode } from './service/RootNode'; 38 | export { Util }; 39 | -------------------------------------------------------------------------------- /packages/core/src/util/replacement/fence.ts: -------------------------------------------------------------------------------- 1 | import { Options } from '../../types'; 2 | 3 | export function fenceReplacement( 4 | content: string, 5 | node: HTMLElement | Document | DocumentFragment | Element, 6 | options: Options 7 | ) { 8 | var className = node.firstChild ? (node.firstChild as Element).className : ''; 9 | var language = (className.match(/language-(\S+)/) || [null, ''])[1]; 10 | var startFence = 11 | options.startFence != undefined ? options.startFence : options.fence; 12 | var endFence = 13 | options.endFence != undefined ? options.endFence : options.fence; 14 | 15 | var parent = node.parentNode; 16 | var parentIsList = parent && parent.nodeName === 'LI'; 17 | return ( 18 | (parentIsList ? '\n' : '\n\n') + 19 | startFence + 20 | language + 21 | '\n' + 22 | (node.firstChild ? node.firstChild.textContent : '') + 23 | ((node.firstChild && 24 | node.firstChild.textContent && 25 | node.firstChild.textContent.endsWith('\n')) || 26 | !content 27 | ? '' 28 | : '\n') + 29 | endFence + 30 | '\n\n' 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /packages/@sitdown/juejin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sitdown/juejin", 3 | "version": "1.1.3", 4 | "description": "Convert 掘金 HTML into Markdown with JavaScript.", 5 | "author": "LinFeng1997 <244732635@qq.com>", 6 | "module": "dist/.esm.js", 7 | "homepage": "https://github.com/mdnice/sitdown#readme", 8 | "license": "MIT", 9 | "main": "dist/index.js", 10 | "typings": "dist/index.d.ts", 11 | "files": [ 12 | "dist" 13 | ], 14 | "publishConfig": { 15 | "registry": "https://registry.npmjs.org/" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/mdnice/sitdown.git" 20 | }, 21 | "scripts": { 22 | "start": "tsdx watch --name src", 23 | "build": "tsdx build --name src", 24 | "test": "tsdx test", 25 | "test:debug": "node --inspect node_modules/.bin/tsdx test --runInBand", 26 | "lint": "tsdx lint --fix" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/mdnice/sitdown/issues" 30 | }, 31 | "devDependencies": { 32 | "sitdown": "^1.1.1" 33 | }, 34 | "gitHead": "ca200d2940d38e15d809a4a879982027f520b4ab" 35 | } 36 | -------------------------------------------------------------------------------- /packages/@sitdown/wechat/test/spec/markdownPaper/fence.html: -------------------------------------------------------------------------------- 1 |
2 |
    3 |
  • 4 |
  • 5 |
  • 6 |
7 |
pre code {  display: -webkit-box !important}
14 |
-------------------------------------------------------------------------------- /packages/core/src/plugins/code.ts: -------------------------------------------------------------------------------- 1 | import Service from '../service'; 2 | import { Options } from '../types'; 3 | 4 | export const applyCodeRule = (service: Service) => { 5 | service.addRule('code', { 6 | filter: function(node) { 7 | var hasSiblings = node.previousSibling || node.nextSibling; 8 | var isCodeBlock = 9 | node.parentNode && node.parentNode.nodeName === 'PRE' && !hasSiblings; 10 | 11 | return node.nodeName === 'CODE' && !isCodeBlock; 12 | }, 13 | 14 | replacement: function(content, _, options: Options) { 15 | if (!content.trim()) return ''; 16 | 17 | var delimiter = options.codeDelimiter || '`'; 18 | var leadingSpace = ''; 19 | var trailingSpace = ''; 20 | var matches = content.match(/`+/gm); 21 | if (matches) { 22 | if (/^`/.test(content)) leadingSpace = ' '; 23 | if (/`$/.test(content)) trailingSpace = ' '; 24 | while (matches.indexOf(delimiter) !== -1) delimiter = delimiter + '`'; 25 | } 26 | 27 | return delimiter + leadingSpace + content + trailingSpace + delimiter; 28 | }, 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 LinFeng1997 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | port: 3200, 3 | extraWatchFiles: [require('path').resolve(__dirname, './nav')], 4 | locales: { 5 | '/': {lang: 'en-US', title: 'SitDown', description: 'Convert HTML into Markdown with JavaScript.'}, 6 | '/zh-hans/': {lang: 'zh-hans', title: 'SitDown', description: 'Convert HTML into Markdown with JavaScript.'} 7 | }, 8 | themeConfig: { 9 | repo: 'mdnice/sitdown', 10 | // 自定义仓库链接文字。默认从 `themeConfig.repo` 中自动推断为 11 | // "GitHub"/"GitLab"/"Bitbucket" 其中之一,或是 "Source"。 12 | repoLabel: 'GitHub', 13 | 14 | // 默认是 false, 设置为 true 来启用 15 | editLinks: true, 16 | // 默认为 "Edit this page" 17 | editLinkText: '帮助我们改善此页面!', 18 | locales: { 19 | '/': { 20 | label: 'English', 21 | selectText: 'Languages', 22 | editLinkText: 'Edit this page on GitHub', 23 | lastUpdated: 'Last Updated', 24 | nav: require('./nav/en.js'), 25 | }, 26 | '/zh-hans/': { 27 | label: '简体中文', 28 | selectText: '选择语言', 29 | lastUpdated: '上次更新', 30 | nav: require('./nav/zh'), 31 | } 32 | }, 33 | $examples: require('../../core/test/spec/gfm') 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "> TODO: description", 6 | "author": "LinFeng1997 <244732635@qq.com>", 7 | "homepage": "https://github.com/mdnice/sitdown#readme", 8 | "license": "ISC", 9 | "main": "lib/docs.js", 10 | "directories": { 11 | "lib": "lib", 12 | "test": "__tests__" 13 | }, 14 | "files": [ 15 | "lib" 16 | ], 17 | "publishConfig": { 18 | "registry": "http://r.cnpmjs.org/" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/mdnice/sitdown.git" 23 | }, 24 | "scripts": { 25 | "test": "echo \"Error: run tests from root\"", 26 | "dev": "vuepress dev", 27 | "build": "vuepress build" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/mdnice/sitdown/issues" 31 | }, 32 | "dependencies": { 33 | "@sitdown/juejin": "^1.1.0", 34 | "@sitdown/wechat": "^1.1.0", 35 | "@sitdown/zhihu": "^1.1.0", 36 | "@sitdown/javascriptweekly": "^1.0.0", 37 | "ant-design-vue": "^1.4.10", 38 | "sitdown": "^1.1.0" 39 | }, 40 | "devDependencies": { 41 | "vuepress": "1.2.0" 42 | }, 43 | "gitHead": "ca200d2940d38e15d809a4a879982027f520b4ab" 44 | } 45 | -------------------------------------------------------------------------------- /packages/core/src/service/RootNode.ts: -------------------------------------------------------------------------------- 1 | import collapseWhitespace from '../util/collapse-whitespace'; 2 | import isBlock from '../util/isBlock'; 3 | import isVoid from '../util/isVoid'; 4 | import HTMLParser from './HTMLParser'; 5 | 6 | var _htmlParser: DOMParser; 7 | 8 | function htmlParser() { 9 | _htmlParser = _htmlParser || new HTMLParser(); 10 | return _htmlParser; 11 | } 12 | 13 | export function createRootNode(input: string) { 14 | var root; 15 | if (typeof input === 'string') { 16 | var doc = htmlParser().parseFromString( 17 | // DOM parsers arrange elements in the and . 18 | // Wrapping in a custom element ensures elements are reliably arranged in 19 | // a single element. 20 | '' + input + '', 21 | 'text/html' 22 | ); 23 | root = doc.getElementById('root-node'); 24 | } else { 25 | root = (input as HTMLElement).cloneNode(true); 26 | } 27 | collapseWhitespace({ 28 | element: root as HTMLElement, 29 | isBlock: isBlock, 30 | isVoid: isVoid, 31 | }); 32 | 33 | return root; 34 | } 35 | 36 | export default class RootNode { 37 | constructor(input: string) { 38 | // @ts-ignore 39 | return createRootNode(input); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/core/test/spec/markdownPaper/table-no-header.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |

TypeScript 3.9 Beta Released — 3.9’s focus is on “performance, polish, and stability” with the most significant change you’re likely to notice being faster compile times.

5 |

Daniel Rosenwasser (Microsoft)

6 |
-------------------------------------------------------------------------------- /packages/@sitdown/javascriptweekly/src/index.ts: -------------------------------------------------------------------------------- 1 | import Service from 'sitdown/dist/service'; 2 | import { Node, Options } from 'sitdown/dist/types'; 3 | // @ts-ignore 4 | import { Util } from 'sitdown/dist/src.esm'; 5 | 6 | const escape = Util.escape; 7 | 8 | const escapes: [ 9 | RegExp, 10 | string | ((substring: string, ...args: any[]) => string) 11 | ][] = [ 12 | [ 13 | /(.*)\|(.*)/g, 14 | (match, p1, p2) => { 15 | if (match.match(/\`.*\|.*\`/)) { 16 | return `${p1}\|${p2}`; 17 | } 18 | return `${p1}\\|${p2}`; 19 | }, 20 | ], 21 | ]; 22 | 23 | function cell(content: string, _node: Node, _options?: Options) { 24 | return escape(escapes, content); 25 | } 26 | 27 | export const applyJSWeeklyRule = (service: Service) => { 28 | service.options.convertNoHeaderTable = true; 29 | service.addRule('JSWeeklyTableRow', { 30 | filter: 'tr', 31 | replacement: function(content) { 32 | var borderCells = ''; 33 | 34 | return '\n' + content + (borderCells ? '\n' + borderCells : ''); 35 | }, 36 | }); 37 | 38 | service.addRule('JSWeeklyTableCell', { 39 | filter: ['th', 'td'], 40 | replacement: function(content: string, node: Node, options) { 41 | return cell(content, node, options); 42 | }, 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /packages/core/src/util/replacement/blank.ts: -------------------------------------------------------------------------------- 1 | // blank 2 | import { isCode } from '../isCode'; 3 | import { isFence } from '../isFence'; 4 | import { isKeep } from '../isKeep'; 5 | import { fenceReplacement } from './fence'; 6 | import { listReplacement } from './list'; 7 | import { keepReplacement } from './keep'; 8 | import { Options, Node } from '../../types'; 9 | 10 | export function blankReplacement( 11 | content: string, 12 | node: Node, 13 | options: Options 14 | ) { 15 | if (isKeep(options, node)) { 16 | return keepReplacement(content, node); 17 | } else if (isFence(options, node)) { 18 | return fenceReplacement(content, node, options); 19 | } else if (isCode(node)) { 20 | var delimiter = options.codeDelimiter ? options.codeDelimiter : '`'; 21 | 22 | return ( 23 | delimiter + 24 | (options.codeBlockStyle === 'fenced' ? ' ' : '') + 25 | (content || ' ') + 26 | delimiter + 27 | '\n' 28 | ); 29 | } else if (node.nodeName.toLowerCase() === 'blockquote') { 30 | return '>'; 31 | } else if (node.nodeName.toLowerCase() === 'li') { 32 | return listReplacement(content, node, options); 33 | } else if (node.nodeName.toLowerCase() === 'ul') { 34 | return content + '\n\n'; 35 | } 36 | return node.isBlock ? content + '\n\n' : ''; 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/src/util/join.ts: -------------------------------------------------------------------------------- 1 | var leadingNewLinesRegExp = /^\n*/; 2 | var trailingNewLinesRegExp = /\n*$/; 3 | 4 | function separatingNewlines(output: string, replacement: string) { 5 | var outputNewLines = output.match(trailingNewLinesRegExp); 6 | var replacementNewLines = replacement.match(leadingNewLinesRegExp); 7 | var newlines = [ 8 | outputNewLines ? outputNewLines[0] : '', 9 | replacementNewLines ? replacementNewLines[0] : '', 10 | ].sort(); 11 | var maxNewlines = newlines[newlines.length - 1]; 12 | return maxNewlines.length < 2 ? maxNewlines : '\n\n'; 13 | } 14 | 15 | /** 16 | * Determines the new lines between the current output and the replacement 17 | * @private 18 | * @param {String} output The current conversion output 19 | * @param {String} replacement The string to append to the output 20 | * @returns The whitespace to separate the current output and the replacement 21 | * @type String 22 | */ 23 | 24 | export default function join(string1: string, string2: string) { 25 | var separator = separatingNewlines(string1, string2); 26 | 27 | // Remove trailing/leading newlines and replace with separator 28 | string1 = string1.replace(trailingNewLinesRegExp, ''); 29 | string2 = string2.replace(leadingNewLinesRegExp, ''); 30 | 31 | return string1 + separator + string2; 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/plugins/link.ts: -------------------------------------------------------------------------------- 1 | import Service from '../service'; 2 | import { escape } from '../util'; 3 | 4 | const specialChars = [' ', '(', ')', '\\', '"']; 5 | const escapes: [RegExp, string][] = [[/"/g, '"']]; 6 | export const applyLinkRule = (service: Service) => { 7 | service.addRule('link', { 8 | filter: function(node, options) { 9 | return !!(options.linkStyle === 'inlined' && node.nodeName === 'A'); 10 | }, 11 | 12 | replacement: function(content, node) { 13 | var href = (node as HTMLElement).getAttribute('href'); 14 | if (!href && !content) { 15 | return ''; 16 | } 17 | // Info:autolink 18 | var normalizeHref = href 19 | ? decodeURIComponent(href).replace('mailto:', '') 20 | : ''; 21 | if (node.firstChild && normalizeHref === node.firstChild.nodeValue) { 22 | return '<' + node.firstChild.nodeValue + '>'; 23 | } 24 | if ( 25 | href && 26 | normalizeHref.split('').some(char => specialChars.includes(char)) 27 | ) { 28 | href = '<' + decodeURIComponent(href) + '>'; 29 | } 30 | var title = escape(escapes, (node as HTMLElement).title); 31 | title = title ? ' "' + title + '"' : ''; 32 | return '[' + content + '](' + href + title + ')'; 33 | }, 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /packages/core/src/service/Node/flankingWhitespace.ts: -------------------------------------------------------------------------------- 1 | import isBlock from '../../util/isBlock'; 2 | import { Node } from '../../types'; 3 | 4 | export default function flankingWhitespace(node: Node) { 5 | var leading = ''; 6 | var trailing = ''; 7 | 8 | if (!node.isBlock) { 9 | var hasLeading = /^[ \r\n\t]/.test(node.textContent || ''); 10 | var hasTrailing = /[ \r\n\t]$/.test(node.textContent || ''); 11 | 12 | if (hasLeading && !isFlankedByWhitespace('left', node)) { 13 | leading = ' '; 14 | } 15 | if (hasTrailing && !isFlankedByWhitespace('right', node)) { 16 | trailing = ' '; 17 | } 18 | } 19 | 20 | return { leading: leading, trailing: trailing }; 21 | } 22 | 23 | function isFlankedByWhitespace(side: string, node: Node) { 24 | var sibling; 25 | var regExp; 26 | var isFlanked; 27 | 28 | if (side === 'left') { 29 | sibling = node.previousSibling; 30 | regExp = / $/; 31 | } else { 32 | sibling = node.nextSibling; 33 | regExp = /^ /; 34 | } 35 | 36 | if (sibling) { 37 | if (sibling.nodeType === 3) { 38 | isFlanked = regExp.test(sibling.nodeValue || ''); 39 | } else if (sibling.nodeType === 1 && !isBlock(sibling as Node)) { 40 | isFlanked = regExp.test(sibling.textContent || ''); 41 | } 42 | } 43 | return isFlanked; 44 | } 45 | -------------------------------------------------------------------------------- /packages/@sitdown/wechat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sitdown/wechat", 3 | "version": "1.1.4", 4 | "description": "Convert 微信 HTML into Markdown with JavaScript.", 5 | "author": "LinFeng1997 <244732635@qq.com>", 6 | "module": "dist/.esm.js", 7 | "homepage": "https://github.com/mdnice/sitdown#readme", 8 | "license": "MIT", 9 | "main": "dist/index.js", 10 | "typings": "dist/index.d.ts", 11 | "files": [ 12 | "dist" 13 | ], 14 | "publishConfig": { 15 | "registry": "https://registry.npmjs.org/" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/mdnice/sitdown.git" 20 | }, 21 | "scripts": { 22 | "start": "tsdx watch --name src", 23 | "build": "tsdx build --name src", 24 | "test": "tsdx test", 25 | "test:debug": "node --inspect ../../../node_modules/.bin/tsdx test --runInBand", 26 | "lint": "tsdx lint --fix", 27 | "patch": "npm run build && npm version patch && npm publish", 28 | "minor": "npm run build && npm version minor && npm publish", 29 | "major": "npm run build && npm version major && npm publish" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/mdnice/sitdown/issues" 33 | }, 34 | "devDependencies": { 35 | "sitdown": "^1.1.1" 36 | }, 37 | "gitHead": "ca200d2940d38e15d809a4a879982027f520b4ab" 38 | } 39 | -------------------------------------------------------------------------------- /packages/@sitdown/zhihu/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sitdown/zhihu", 3 | "version": "1.1.2", 4 | "description": "Convert 知乎 HTML into Markdown with JavaScript.", 5 | "author": "LinFeng1997 <244732635@qq.com>", 6 | "module": "dist/.esm.js", 7 | "homepage": "https://github.com/mdnice/sitdown#readme", 8 | "license": "MIT", 9 | "main": "dist/index.js", 10 | "typings": "dist/index.d.ts", 11 | "files": [ 12 | "dist" 13 | ], 14 | "publishConfig": { 15 | "registry": "https://registry.npmjs.org/" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/mdnice/sitdown.git" 20 | }, 21 | "scripts": { 22 | "start": "tsdx watch --name src", 23 | "build": "tsdx build --name src", 24 | "test": "tsdx test", 25 | "test:debug": "node --inspect ../../../node_modules/.bin/tsdx test --runInBand", 26 | "lint": "tsdx lint --fix", 27 | "patch": "npm run build && npm version patch && npm publish", 28 | "minor": "npm run build && npm version minor && npm publish", 29 | "major": "npm run build && npm version major && npm publish" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/mdnice/sitdown/issues" 33 | }, 34 | "devDependencies": { 35 | "sitdown": "^1.1.1" 36 | }, 37 | "gitHead": "ca200d2940d38e15d809a4a879982027f520b4ab" 38 | } 39 | -------------------------------------------------------------------------------- /packages/@sitdown/zhihu/src/index.ts: -------------------------------------------------------------------------------- 1 | import Service from 'sitdown/dist/service'; 2 | import { applyParagraphRule } from './p'; 3 | 4 | export const applyZhihuRule = (service: Service) => { 5 | service.addRule('zhizhuImg', { 6 | filter: 'img', 7 | 8 | replacement: function( 9 | _content: string, 10 | node: HTMLElement & { isFormula?: boolean } 11 | ) { 12 | var formula = node.getAttribute('data-formula'); 13 | // Info:这个图片是公式 14 | if (formula) { 15 | var isBlockFormula = 16 | node.parentElement && 17 | node.parentElement.nodeName === 'P' && 18 | node.parentElement.innerHTML === node.outerHTML; 19 | 20 | node.isFormula = true; 21 | return isBlockFormula ? `$$\n${formula}\n$$` : `$${formula}$`; 22 | } 23 | var alt = node.getAttribute('alt') || ''; 24 | var src = 25 | node.getAttribute('data-actualsrc') || node.getAttribute('src') || ''; 26 | var title = node.title || ''; 27 | var titlePart = title ? ' "' + title + '"' : ''; 28 | return src ? '![' + alt + ']' + '(' + src + titlePart + ')' : ''; 29 | }, 30 | }); 31 | service.addRule('zhihuNoscript', { 32 | filter: 'noscript', 33 | 34 | replacement: function() { 35 | return ''; 36 | }, 37 | }); 38 | applyParagraphRule(service); 39 | }; 40 | -------------------------------------------------------------------------------- /packages/@sitdown/javascriptweekly/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sitdown/javascriptweekly", 3 | "version": "1.0.3", 4 | "description": "Convert javascriptweekly HTML into Markdown with JavaScript.", 5 | "author": "LinFeng1997 <244732635@qq.com>", 6 | "module": "dist/.esm.js", 7 | "homepage": "https://github.com/mdnice/sitdown#readme", 8 | "license": "MIT", 9 | "main": "dist/index.js", 10 | "typings": "dist/index.d.ts", 11 | "files": [ 12 | "dist" 13 | ], 14 | "publishConfig": { 15 | "registry": "https://registry.npmjs.org/" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/mdnice/sitdown.git" 20 | }, 21 | "scripts": { 22 | "start": "tsdx watch --name src", 23 | "build": "tsdx build --name src", 24 | "test": "tsdx test", 25 | "test:debug": "node --inspect node_modules/.bin/tsdx test --runInBand", 26 | "lint": "tsdx lint --fix", 27 | "patch": "npm run build && npm version patch && npm publish", 28 | "minor": "npm run build && npm version minor && npm publish", 29 | "major": "npm run build && npm version major && npm publish" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/mdnice/sitdown/issues" 33 | }, 34 | "peerDependencies": { 35 | "sitdown": "^1.1.1" 36 | }, 37 | "gitHead": "ca200d2940d38e15d809a4a879982027f520b4ab" 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "workspaces": [ 3 | "packages/core", 4 | "packages/docs", 5 | "packages/@sitdown/*" 6 | ], 7 | "private": true, 8 | "license": "MIT", 9 | "scripts": { 10 | "bootstrap": "lerna bootstrap", 11 | "start": "lerna run start", 12 | "build": "lerna exec --scope sitdown -- yarn build && lerna run build --ignore sitdown", 13 | "test": "lerna run test", 14 | "lint": "tsdx lint --fix", 15 | "docs:dev": "lerna exec --scope sitdown -- yarn build && lerna exec --scope docs -- vuepress dev", 16 | "docs:build": "lerna exec --scope sitdown -- yarn build && lerna exec --scope docs -- vuepress build", 17 | "clean": "lerna clean && rm -rf node_modules", 18 | "prepublish": "npm run build && lerna publish" 19 | }, 20 | "husky": { 21 | "hooks": { 22 | "pre-commit": "npm run lint" 23 | } 24 | }, 25 | "prettier": { 26 | "printWidth": 80, 27 | "semi": true, 28 | "singleQuote": true, 29 | "trailingComma": "es5" 30 | }, 31 | "devDependencies": { 32 | "@types/jest": "^24.0.25", 33 | "@types/markdown-it": "0.0.9", 34 | "husky": "^3.1.0", 35 | "jest-raw-loader": "^1.0.1", 36 | "lerna": "^3.20.2", 37 | "markdown-it": "^12.3.2", 38 | "tsdx": "^0.12.0", 39 | "tslib": "^1.10.0", 40 | "typescript": "^3.7.4", 41 | "vuepress": "^1.2.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/docs/.vuepress/util/highlight.js: -------------------------------------------------------------------------------- 1 | import prism from 'prismjs'; 2 | // import loadLanguages from 'prismjs/components/index'; 3 | import escapeHtml from 'escape-html'; 4 | 5 | // required to make embedded highlighting work... 6 | // loadLanguages(['markup', 'css', 'javascript']) 7 | 8 | function wrap (code, lang) { 9 | if (lang === 'text') { 10 | code = escapeHtml(code) 11 | } 12 | return `
${code}
` 13 | } 14 | 15 | export default (str, lang) => { 16 | if (!lang) { 17 | return wrap(str, 'text') 18 | } 19 | lang = lang.toLowerCase() 20 | const rawLang = lang 21 | if (lang === 'vue' || lang === 'html') { 22 | lang = 'markup' 23 | } 24 | if (lang === 'md') { 25 | lang = 'markdown' 26 | } 27 | if (lang === 'rb') { 28 | lang = 'ruby' 29 | } 30 | if (lang === 'ts') { 31 | lang = 'typescript' 32 | } 33 | if (lang === 'py') { 34 | lang = 'python' 35 | } 36 | if (lang === 'sh') { 37 | lang = 'bash' 38 | } 39 | if (lang === 'yml') { 40 | lang = 'yaml' 41 | } 42 | if (lang === 'styl') { 43 | lang = 'stylus' 44 | } 45 | 46 | if (!prism.languages[lang]) { 47 | // console.log('不存在的语言'); 48 | } 49 | if (prism.languages[lang]) { 50 | const code = prism.highlight(str, prism.languages[lang], lang) 51 | return wrap(code, rawLang) 52 | } 53 | return wrap(str, 'text') 54 | } 55 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sitdown", 3 | "version": "1.1.7", 4 | "description": "Convert HTML into Markdown with JavaScript.Support GitHub Flavored Markdown Spec.Also support almost wechat/zhihu/csdn/juejin HTML.", 5 | "author": "LinFeng1997 <244732635@qq.com>", 6 | "module": "dist/src.esm.js", 7 | "homepage": "https://github.com/mdnice/sitdown#readme", 8 | "license": "MIT", 9 | "main": "dist/index.js", 10 | "typings": "dist/index.d.ts", 11 | "files": [ 12 | "dist" 13 | ], 14 | "publishConfig": { 15 | "registry": "https://registry.npmjs.org/" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/mdnice/sitdown.git" 20 | }, 21 | "scripts": { 22 | "start": "tsdx watch --name src", 23 | "build": "tsdx build --name src", 24 | "test": "tsdx test", 25 | "test:debug": "node --inspect ../../node_modules/.bin/tsdx test --runInBand", 26 | "lint": "tsdx lint src --fix", 27 | "patch": "npm run build && npm version patch && npm publish", 28 | "minor": "npm run build && npm version minor && npm publish", 29 | "major": "npm run build && npm version major && npm publish" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/mdnice/sitdown/issues" 33 | }, 34 | "browser": { 35 | "jsdom": false 36 | }, 37 | "dependencies": { 38 | "jsdom": "^16.5.0" 39 | }, 40 | "gitHead": "ca200d2940d38e15d809a4a879982027f520b4ab" 41 | } 42 | -------------------------------------------------------------------------------- /packages/@sitdown/zhihu/test/zhihu.test.ts: -------------------------------------------------------------------------------- 1 | import { Sitdown } from 'sitdown'; 2 | import { applyZhihuRule } from '../src'; 3 | import md from './spec/markdownPaper/paper1-zhihu.md'; 4 | import html from './spec/markdownPaper/paper1-zhihu.html'; 5 | import md2 from './spec/markdownPaper/paper2-zhihu.md'; 6 | import html2 from './spec/markdownPaper/paper2-zhihu.html'; 7 | import md3 from './spec/markdownPaper/paper3-zhihu.md'; 8 | import html3 from './spec/markdownPaper/paper3-zhihu.html'; 9 | import md4 from './spec/markdownPaper/formula.md'; 10 | import html4 from './spec/markdownPaper/formula.html'; 11 | 12 | /* 13 | 知乎 html 转 md 存在的不一致处: 14 | 1. 知乎进行了中文排版美化,如数字与中文、中文与英文间有空格 15 | 2. 图片转储了,并在 noscript 里有一份备份 16 | 3. 图片、链接描述(alt)没了 17 | 4. 知乎将公式转成图片了 18 | 5. 知乎把强调的链接的强调给滤掉了 19 | */ 20 | describe('知乎', () => { 21 | let sitdown = new Sitdown({ 22 | keepFilter: ['style'], 23 | codeBlockStyle: 'fenced', 24 | bulletListMarker: '-', 25 | hr: '---', 26 | }); 27 | sitdown.use(applyZhihuRule); 28 | it('paper1 works', () => { 29 | const expected = sitdown.HTMLToMD(html); 30 | expect(expected).toEqual(md); 31 | }); 32 | 33 | it('paper2 works', () => { 34 | const expected = sitdown.HTMLToMD(html2); 35 | expect(expected).toEqual(md2); 36 | }); 37 | 38 | it('paper3 works', () => { 39 | const expected = sitdown.HTMLToMD(html3); 40 | expect(expected).toEqual(md3); 41 | }); 42 | 43 | it('formula', () => { 44 | const expected = sitdown.HTMLToMD(html4); 45 | expect(expected).toEqual(md4); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /packages/docs/Plugin.md: -------------------------------------------------------------------------------- 1 | ## `@sitdown/juejin` 2 | 3 | ```ts 4 | import { Sitdown } from 'sitdown'; 5 | import { applyJuejinRule } from '@sitdown/juejin'; 6 | 7 | let sitdown = new Sitdown({ 8 | keepFilter: ['style'], 9 | codeBlockStyle: 'fenced', 10 | bulletListMarker: '-', 11 | hr: '---', 12 | }); 13 | sitdown.use(applyJuejinRule); 14 | ``` 15 | 16 | ## `@sitdown/wechat` 17 | 18 | ```ts 19 | import { Sitdown,RootNode } from 'sitdown'; 20 | import { applyWechatRule, extraFootLinks } from '@sitdown/wechat'; 21 | 22 | let sitdown = new Sitdown({ 23 | keepFilter: ['style'], 24 | codeBlockStyle: 'fenced', 25 | bulletListMarker: '-', 26 | hr: '---', 27 | }); 28 | sitdown.use(applyWechatRule); 29 | ``` 30 | 31 | support mdnice wechat footlink: 32 | ```ts 33 | import { extraFootLinks } from '@sitdown/wechat'; 34 | 35 | const wechatToMD = (html: string) => { 36 | const root = new sitdown.RootNode(html); 37 | const footLinks = extraFootLinks(root); 38 | return sitdown.HTMLToMD(html, { footLinks }); 39 | }; 40 | ``` 41 | 42 | ## `@sitdown/zhihu` 43 | 44 | ```ts 45 | import { Sitdown } from 'sitdown'; 46 | import { applyZhihuRule } from '@sitdown/zhihu'; 47 | let sitdown = new Sitdown({ 48 | keepFilter: ['style'], 49 | codeBlockStyle: 'fenced', 50 | bulletListMarker: '-', 51 | hr: '---', 52 | }); 53 | sitdown.use(applyZhihuRule); 54 | 55 | ``` 56 | 57 | ## csdn 58 | 59 | ```ts 60 | import { Sitdown } from 'sitdown'; 61 | 62 | let sitdown = new Sitdown({ 63 | keepFilter: ['style'], 64 | codeBlockStyle: 'fenced', 65 | bulletListMarker: '-', 66 | hr: '---', 67 | }); 68 | ``` 69 | -------------------------------------------------------------------------------- /packages/@sitdown/javascriptweekly/test/spec/markdownPaper/paper1.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

ECMAScript 2020: The Final Feature Set — TC39 has just approved the ECMAScript 2020 spec (a full weekend's bedtime reading right there!) with Ecma GA approval due in a few months, but what’s new? Dr. Axel rounds it up with links to the included stage 4 proposals. If you prefer something more code-driven, Pawel Grzybek has a similar roundup.

4 |

Dr. Axel Rauschmayer

5 |
-------------------------------------------------------------------------------- /packages/core/src/plugins/indentedCodeBlock.ts: -------------------------------------------------------------------------------- 1 | import { Options, Node } from '../types'; 2 | import { 3 | findOrderListIndentNumber, 4 | findParentNumber, 5 | repeat, 6 | IndentCodeIsListfirstChild, 7 | } from '../util'; 8 | import Service from '../service'; 9 | 10 | function caclListIndent(node: Node, options: Options): number { 11 | var nestULCount = findParentNumber(node, 'UL'); 12 | var nestOLCount = findParentNumber(node, 'OL'); 13 | if (nestOLCount) { 14 | // Info:如果这个缩进代码父元素是有序列表,并它是第一个元素 15 | const parentNode = node.parentNode; 16 | const isFirstChild = 17 | parentNode && 18 | parentNode.firstChild && 19 | parentNode.nodeName === 'LI' && 20 | parentNode.firstChild === node; 21 | const IndentCodeIsfirstChild = IndentCodeIsListfirstChild( 22 | parentNode as HTMLElement, 23 | options 24 | ); 25 | return ( 26 | nestULCount * 2 + 27 | nestOLCount * 4 + 28 | 4 + 29 | findOrderListIndentNumber(node) + 30 | (isFirstChild ? -4 : 0) + 31 | (IndentCodeIsfirstChild ? -1 : 0) 32 | ); 33 | } 34 | return nestULCount * 2 + 4; 35 | } 36 | export const applyIndentedCodeBlockRule = (service: Service) => { 37 | service.addRule('indentedCodeBlock', { 38 | filter: function(node, options) { 39 | return !!( 40 | options.codeBlockStyle === 'indented' && 41 | node.nodeName === 'PRE' && 42 | node.firstChild && 43 | node.firstChild.nodeName === 'CODE' 44 | ); 45 | }, 46 | 47 | replacement: function(_: string, node, options: Options) { 48 | const indent = repeat(' ', caclListIndent(node, options)); 49 | return node.firstChild && node.firstChild.textContent 50 | ? '\n\n' + 51 | indent + 52 | node.firstChild.textContent.replace(/\n/g, '\n' + indent) + 53 | '\n\n' 54 | : '\n\n \n\n'; 55 | }, 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /packages/core/test/gfm.test.ts: -------------------------------------------------------------------------------- 1 | // test 2 | import { Sitdown, RootNode } from '../src'; 3 | import Examples from './spec/gfm'; 4 | import Service from '../src/service'; 5 | import {Options,Node} from '../src/types'; 6 | import MarkdownIt from 'markdown-it'; 7 | // import { applyZhihuRule } from '@sitdown/zhihu/src'; 8 | 9 | const taskList = require('./spec/md-it-plugin-taskList'); 10 | const md = new MarkdownIt({ 11 | html: true, 12 | }).use(taskList); 13 | interface Example { 14 | index: number; 15 | md: string; 16 | html: string; 17 | option?: Options; 18 | enhance?: (service: Service) => void; 19 | } 20 | 21 | Examples.forEach(example => { 22 | // Info:替换制表符和结尾的两个换行符 23 | example.md = example.md.replace(/→/g, '\t').replace(/\n+$/, ''); 24 | example.html = example.html.replace(/→/g, '\t').replace(/\n+$/, ''); 25 | }); 26 | 27 | describe('GFM', () => { 28 | (Examples as Example[]) 29 | .filter(example => example.index <= 673) 30 | // .filter( 31 | // example => 32 | // example.index > 652 && example.index <= 673 && example.index !== 636 33 | // ) 34 | .forEach(example => { 35 | let sitdown = new Sitdown(); 36 | // sitdown.use(applyZhihuRule as any); 37 | it(`gfm example${example.index} html to markdown works`, () => { 38 | if (example.option) { 39 | sitdown = new Sitdown(example.option); 40 | } 41 | const expected = sitdown.HTMLToMD(example.html); 42 | // console.log('expected\n\n',expected); 43 | // console.log('example.md\n\n',example.md); 44 | expect(expected).toEqual(example.md); 45 | }); 46 | 47 | it(`gfm example${example.index} markdown to html works`, () => { 48 | const html = md.render(example.md); 49 | expect((new RootNode(html) as Node).innerHTML).toEqual( 50 | (new RootNode(html) as Node).innerHTML 51 | ); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /packages/core/src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import Service from '../service'; 2 | import { applyListRule } from './list'; 3 | import { applyHrRule } from './hr'; 4 | import { applyParagraphRule } from './paragraph'; 5 | import { applyHeadingRule } from './heading'; 6 | import { applyFenceRule } from './fencedCodeBlock'; 7 | import { applyCodeRule } from './code'; 8 | import { applyReferenceLinkRule } from './referenceLinks'; 9 | import { applyTableRule } from './table'; 10 | import { applyIndentedCodeBlockRule } from './indentedCodeBlock'; 11 | import { applyBlockquoteRule } from './blockquote'; 12 | import { applyEmRule } from './em'; 13 | import { applyDelRule } from './del'; 14 | import { applyLinkRule } from './link'; 15 | import { applyImageRule } from './image'; 16 | import { applyBrRule } from './br'; 17 | import { applyStrongRule } from './strong'; 18 | import { applyTaskRule } from './taskListItems'; 19 | 20 | import { isKeep } from '../util/isKeep'; 21 | 22 | export default (service: Service) => { 23 | service.use([ 24 | applyListRule, 25 | applyHrRule, 26 | applyParagraphRule, 27 | applyHeadingRule, 28 | applyFenceRule, 29 | applyCodeRule, 30 | applyReferenceLinkRule, 31 | applyTableRule, 32 | applyIndentedCodeBlockRule, 33 | applyBlockquoteRule, 34 | applyEmRule, 35 | applyDelRule, 36 | applyLinkRule, 37 | applyBrRule, 38 | applyStrongRule, 39 | applyImageRule, 40 | applyTaskRule, 41 | ]); 42 | 43 | service.keep(node => { 44 | if (isKeep(service.options, node)) { 45 | if (node.parentNode) { 46 | const index = Array.from(node.parentNode.childNodes).findIndex( 47 | n => n === node 48 | ); 49 | const next: ChildNode & { unNeedEscape?: boolean } = 50 | node.parentNode.childNodes[index + 1]; 51 | next && (next.unNeedEscape = true); 52 | } 53 | return true; 54 | } 55 | return false; 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /packages/core/src/plugins/referenceLinks.ts: -------------------------------------------------------------------------------- 1 | import Service from '../service'; 2 | import { escape } from '../util'; 3 | 4 | const escapes: [RegExp, string][] = [ 5 | [/\*/g, '\\*'], 6 | [/"/g, '\\"'], 7 | ]; 8 | export const applyReferenceLinkRule = (service: Service) => { 9 | service.addRule('referenceLink', { 10 | filter: function(node, options) { 11 | return !!( 12 | options.linkStyle === 'referenced' && 13 | node.nodeName === 'A' && 14 | node.getAttribute('href') 15 | ); 16 | }, 17 | 18 | replacement: function(content, node: any, options) { 19 | var href = escape(escapes, decodeURIComponent(node.getAttribute('href'))); 20 | if (href.includes(' ')) { 21 | href = '<' + href + '>'; 22 | } 23 | var title = node.title ? ' "' + escape(escapes, node.title) + '"' : ''; 24 | var replacement; 25 | var reference; 26 | 27 | switch (options.linkReferenceStyle) { 28 | case 'collapsed': 29 | replacement = '[' + content + '][]'; 30 | reference = '[' + content + ']: ' + href + title; 31 | break; 32 | case 'shortcut': 33 | replacement = '[' + content + ']'; 34 | reference = '[' + content + ']: ' + href + title; 35 | break; 36 | default: 37 | var id = 38 | this.references && this.references.length 39 | ? 'ref' + (this.references.length + 1) 40 | : 'ref'; 41 | replacement = '[' + content + '][' + id + ']'; 42 | reference = '[' + id + ']: ' + href + title; 43 | } 44 | 45 | this.references && this.references.push(reference); 46 | return replacement; 47 | }, 48 | 49 | references: [], 50 | 51 | unshift: function() { 52 | var references = ''; 53 | if (this.references && this.references.length) { 54 | references = '\n\n' + this.references.join('\n') + '\n\n'; 55 | this.references = []; // Reset references 56 | } 57 | return references; 58 | }, 59 | }); 60 | }; 61 | -------------------------------------------------------------------------------- /packages/core/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface Options { 2 | env?: any; 3 | headingStyle?: 'setext' | 'atx'; 4 | hr?: string; 5 | br?: string; 6 | bulletListMarker?: '-' | '+' | '*'; 7 | codeBlockStyle?: 'indented' | 'fenced'; 8 | emDelimiter?: '_' | '*'; 9 | fence?: '```' | '~~~'; 10 | startFence?: '```' | '~~~'; 11 | endFence?: '```' | '~~~'; 12 | codeDelimiter?: string; 13 | strongDelimiter?: '__' | '**'; 14 | linkStyle?: 'inlined' | 'referenced'; 15 | linkReferenceStyle?: 'full' | 'collapsed' | 'shortcut'; 16 | convertNoHeaderTable?: boolean; 17 | 18 | keepReplacement?: ReplacementFunction; 19 | blankReplacement?: ReplacementFunction; 20 | defaultReplacement?: ReplacementFunction; 21 | keepFilter?: Filter; 22 | rules?: { 23 | [key: string]: Rule; 24 | }; 25 | } 26 | 27 | export interface Rule { 28 | references?: string[]; 29 | filter: Filter; 30 | replacement?: ReplacementFunction; 31 | append?: () => void; 32 | unshift?: () => void; 33 | } 34 | export type Filter = TagName | TagName[] | FilterFunction; 35 | export type FilterFunction = (node: HTMLElement, options: Options) => boolean; 36 | 37 | export type ReplacementFunction = ( 38 | content: string, 39 | node: Node, 40 | options: Options 41 | ) => string; 42 | 43 | export type TagName = keyof HTMLElementTagNameMap; 44 | 45 | export type Node = HTMLElement & { 46 | unNeedEscape?: boolean; 47 | data?: string; 48 | isBlank?: boolean; 49 | isBlock?: boolean; 50 | isCode?: boolean; 51 | flankingWhitespace?: { 52 | leading: string; 53 | trailing: string; 54 | }; 55 | }; 56 | 57 | export interface Rules { 58 | options: Options; 59 | array: Rule[]; 60 | 61 | blankRule: { 62 | replacement: ReplacementFunction; 63 | }; 64 | defaultRule: { 65 | replacement: ReplacementFunction; 66 | }; 67 | keepReplacement: ReplacementFunction; 68 | 69 | add(key: Filter, rule: Rule): void; 70 | forEach(callback: (rule: Rule, index: number) => any): void; 71 | forNode(node: Node): Rule; 72 | keep(filter: Filter): void; 73 | remove(filter: Filter): void; 74 | } 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | .idea 107 | 108 | .DS_Store -------------------------------------------------------------------------------- /packages/@sitdown/wechat/test/wechat.test.ts: -------------------------------------------------------------------------------- 1 | import { Sitdown, RootNode } from 'sitdown'; 2 | import { Node } from 'sitdown/dist/types'; 3 | import { applyWechatRule, extraFootLinks } from '../src'; 4 | // import html from './spec/temp.html'; 5 | // import md5 from './spec/markdownPaper/paper5.md'; 6 | // import html5 from './spec/markdownPaper/paper5-wechat.html'; 7 | 8 | /* 9 | 微信 html 转 md 存在的不一致处: 10 | 1. 图片转储,且访问有鉴权 11 | 2. 图片描述放在 figcaption 里 12 | 3. 微信不支持外链,所以用脚注的方式兼容。 13 | 4. 代码块转回来永远是一行。 14 | 5. 微信有独创的居中块,html 是 span 带有 `display:block;text-align:center;` 样式 15 | 6. 公式用 svg 画出来的 16 | 7. 文章开头的作者信息和结束的信息,包裹在 section 里 17 | */ 18 | describe('微信', () => { 19 | let sitdown = new Sitdown({ 20 | keepFilter: ['style'], 21 | codeBlockStyle: 'fenced', 22 | bulletListMarker: '-', 23 | hr: '---', 24 | }); 25 | sitdown.use(applyWechatRule); 26 | const wechatToMD = (html: string) => { 27 | const root = new RootNode(html) as Node; 28 | const footLinks = extraFootLinks(root); 29 | return sitdown.HTMLToMD(html, { footLinks }); 30 | }; 31 | 32 | it('formula works', () => { 33 | const expected = wechatToMD(require('./spec/markdownPaper/formula.html')); 34 | expect(expected).toEqual(require('./spec/markdownPaper/formula.md')); 35 | }); 36 | 37 | it('paper1 works', () => { 38 | const expected = wechatToMD( 39 | require('./spec/markdownPaper/paper1-wechat.html') 40 | ); 41 | expect(expected).toEqual(require('./spec/markdownPaper/paper1-wechat.md')); 42 | }); 43 | 44 | it('fence works', () => { 45 | const expected = wechatToMD(require('./spec/markdownPaper/fence.html')); 46 | expect(expected).toEqual(require('./spec/markdownPaper/fence.md')); 47 | }); 48 | 49 | it('paper4 works', () => { 50 | const expected = wechatToMD( 51 | require('./spec/markdownPaper/paper4-wechat.html') 52 | ); 53 | expect(expected).toEqual(require('./spec/markdownPaper/paper4-wechat.md')); 54 | }); 55 | 56 | // it('paper5 works', () => { 57 | // const expected = wechatToMD(html5); 58 | // require('fs').writeFileSync( 59 | // 'test/spec/markdownPaper/paper5-wechat.md', 60 | // expected, 61 | // 'utf-8' 62 | // ); 63 | // expect(expected).toEqual(md5); 64 | // }); 65 | }); 66 | -------------------------------------------------------------------------------- /packages/docs/zh-hans/插件.md: -------------------------------------------------------------------------------- 1 | ## `@sitdown/juejin` 2 | 3 | 掘金 html 转 md 注意处: 4 | 1. 掘金进行了中文排版美化,如数字与中文、中文与英文间有空格 5 | 2. 掘金对图片进行了转储,地址存储在 img 标签的 data-src 下 6 | 3. 掘金对代码块做了处理,多了一个复制代码 7 | 8 | ```ts 9 | import { Sitdown } from 'sitdown'; 10 | import { applyJuejinRule } from '@sitdown/juejin'; 11 | 12 | let sitdown = new Sitdown({ 13 | keepFilter: ['style'], 14 | codeBlockStyle: 'fenced', 15 | bulletListMarker: '-', 16 | hr: '---', 17 | }); 18 | sitdown.use(applyJuejinRule); 19 | ``` 20 | 21 | ## `@sitdown/wechat` 22 | 23 | 微信 html 转 md 注意处: 24 | 1. 图片转储,且访问有鉴权 25 | 2. 图片描述放在 figcaption 里 26 | 3. 微信不支持外链,所以用脚注的方式兼容。 27 | 4. 代码块转回来永远是一行。 28 | 5. 微信有独创的居中块,html 是 span 带有 `display:block;text-align:center;` 样式 29 | 6. 公式用 svg 画出来的 30 | 7. 文章开头的作者信息和结束的信息,包裹在 section 里 31 | 32 | ```ts 33 | import { Sitdown,RootNode } from 'sitdown'; 34 | import { applyWechatRule, extraFootLinks } from '@sitdown/wechat'; 35 | 36 | let sitdown = new Sitdown({ 37 | keepFilter: ['style'], 38 | codeBlockStyle: 'fenced', 39 | bulletListMarker: '-', 40 | hr: '---', 41 | }); 42 | sitdown.use(applyWechatRule); 43 | ``` 44 | 45 | 支持 mdnice 微信脚注: 46 | ```ts 47 | import { extraFootLinks } from '@sitdown/wechat'; 48 | 49 | const wechatToMD = (html: string) => { 50 | const root = new sitdown.RootNode(html); 51 | const footLinks = extraFootLinks(root); 52 | return sitdown.HTMLToMD(html, { footLinks }); 53 | }; 54 | ``` 55 | 56 | ## `@sitdown/zhihu` 57 | 58 | 知乎 html 转 md 注意处: 59 | 1. 知乎进行了中文排版美化,如数字与中文、中文与英文间有空格 60 | 2. 图片转储了,并在 noscript 里有一份备份 61 | 3. 图片、链接描述(alt)没了 62 | 4. 知乎将公式转成图片了 63 | 5. 知乎把强调的链接的强调给滤掉了 64 | 65 | ```ts 66 | import { Sitdown } from 'sitdown'; 67 | import { applyZhihuRule } from '@sitdown/zhihu'; 68 | let sitdown = new Sitdown({ 69 | keepFilter: ['style'], 70 | codeBlockStyle: 'fenced', 71 | bulletListMarker: '-', 72 | hr: '---', 73 | }); 74 | sitdown.use(applyZhihuRule); 75 | 76 | ``` 77 | 78 | ## csdn 79 | 80 | csdn html 转 md 注意处: 81 | 1. csdn 开头有多余的注释 82 | 2. csdn 的标题带了很多空链接 83 | 84 | ```ts 85 | import { Sitdown } from 'sitdown'; 86 | 87 | let sitdown = new Sitdown({ 88 | keepFilter: ['style'], 89 | codeBlockStyle: 'fenced', 90 | bulletListMarker: '-', 91 | hr: '---', 92 | }); 93 | ``` 94 | -------------------------------------------------------------------------------- /packages/@sitdown/wechat/src/fence.ts: -------------------------------------------------------------------------------- 1 | import Service from 'sitdown/dist/service'; 2 | // @ts-ignore 3 | import { Util } from 'sitdown/dist/src.esm'; 4 | 5 | const isFence = Util.isFence; 6 | function fenceReplacement( 7 | content: string, 8 | node: HTMLElement | Document | DocumentFragment | Element, 9 | options: Service['options'] 10 | ) { 11 | var className = node.firstChild ? (node.firstChild as Element).className : ''; 12 | var language = (className.match(/language-(\S+)/) || [null, ''])[1]; 13 | 14 | const isWechatFence = (node as Element).className.match(/code-snippet/); 15 | if (isWechatFence) { 16 | const wechatFenceLang = (node as Element).getAttribute('data-lang'); 17 | language = wechatFenceLang; 18 | } 19 | 20 | var startFence = 21 | options.startFence != undefined ? options.startFence : options.fence; 22 | var endFence = 23 | options.endFence != undefined ? options.endFence : options.fence; 24 | 25 | var parent = node.parentNode; 26 | var parentIsList = parent && parent.nodeName === 'LI'; 27 | let code = node.firstChild ? node.firstChild.textContent : ''; 28 | 29 | if (isWechatFence) { 30 | code = Array.from(node.children) 31 | .map(item => item.textContent) 32 | .join('\n'); 33 | } 34 | return ( 35 | (parentIsList ? '\n' : '\n\n') + 36 | startFence + 37 | language + 38 | '\n' + 39 | code + 40 | ((node.firstChild && 41 | node.firstChild.textContent && 42 | node.firstChild.textContent.endsWith('\n')) || 43 | !content 44 | ? '' 45 | : '\n') + 46 | endFence + 47 | '\n\n' 48 | ); 49 | } 50 | export const applyFenceRule = (service: Service) => { 51 | // 1. blankReplacement ul and li 52 | const oldBlankReplace = service.rules.blankRule.replacement; 53 | service.rules.blankRule = { 54 | replacement: (content, node, options) => { 55 | const parent = node.parentElement; 56 | const isWechatFenceIndex = 57 | parent && 58 | parent.nodeName === 'UL' && 59 | node.nodeName === 'LI' && 60 | parent.className.includes('code-snippet__line-index'); 61 | if (isWechatFenceIndex) { 62 | return ''; 63 | } 64 | return oldBlankReplace.call(service.rules, content, node, options); 65 | }, 66 | }; 67 | // 2. pre and code span replacement 68 | service.addRule('fencedCodeBlock', { 69 | filter: function(node, options) { 70 | return isFence(options, node); 71 | }, 72 | 73 | replacement: fenceReplacement, 74 | }); 75 | }; 76 | -------------------------------------------------------------------------------- /packages/core/src/service/Rules/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Manages a collection of rules used to convert HTML to Markdown 3 | */ 4 | import findRule from './findRule'; 5 | import { 6 | Options, 7 | ReplacementFunction, 8 | Rule, 9 | Node, 10 | FilterFunction, 11 | } from '../../types'; 12 | 13 | export default class Rules { 14 | private _keep: Rule[]; 15 | private _remove: Rule[]; 16 | 17 | options: Options; 18 | array: Rule[]; 19 | 20 | blankRule: { 21 | replacement: ReplacementFunction; 22 | }; 23 | defaultRule: { 24 | replacement: ReplacementFunction; 25 | }; 26 | keepReplacement: ReplacementFunction; 27 | 28 | constructor(options: Options) { 29 | if (typeof options.blankReplacement !== 'function') { 30 | throw Error('blankReplacement option must be function'); 31 | } 32 | 33 | if (typeof options.keepReplacement !== 'function') { 34 | throw Error('keepReplacement option must be function'); 35 | } 36 | 37 | if (typeof options.defaultReplacement !== 'function') { 38 | throw Error('defaultReplacement option must be function'); 39 | } 40 | this.options = options; 41 | this._keep = []; 42 | this._remove = []; 43 | 44 | this.blankRule = { 45 | replacement: options.blankReplacement, 46 | }; 47 | 48 | this.keepReplacement = options.keepReplacement; 49 | 50 | this.defaultRule = { 51 | replacement: options.defaultReplacement, 52 | }; 53 | 54 | this.array = []; 55 | for (var key in options.rules) this.array.push(options.rules[key]); 56 | } 57 | 58 | add(_key: string, rule: Rule) { 59 | this.array.unshift(rule); 60 | } 61 | 62 | keep(filter: FilterFunction) { 63 | this._keep.unshift({ 64 | filter: filter, 65 | replacement: this.keepReplacement, 66 | }); 67 | } 68 | 69 | remove(filter: FilterFunction) { 70 | this._remove.unshift({ 71 | filter: filter, 72 | replacement() { 73 | return ''; 74 | }, 75 | }); 76 | } 77 | 78 | forNode(node: Node) { 79 | if (node.isBlank) return this.blankRule; 80 | var rule; 81 | 82 | if ((rule = findRule(this.array, node, this.options))) return rule; 83 | if ((rule = findRule(this._keep, node, this.options))) return rule; 84 | if ((rule = findRule(this._remove, node, this.options))) return rule; 85 | 86 | return this.defaultRule; 87 | } 88 | 89 | forEach(fn: (item: any, i: number) => void) { 90 | for (var i = 0; i < this.array.length; i++) fn(this.array[i], i); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /packages/@sitdown/wechat/test/spec/markdownPaper/formula.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/docs/.vuepress/components/Example.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 87 | -------------------------------------------------------------------------------- /packages/core/src/util/replacement/list/listNode.ts: -------------------------------------------------------------------------------- 1 | import { Node, Options } from '../../../types'; 2 | import { findParentNumber } from '../../findParentNumber'; 3 | import { repeat } from '../../repeat'; 4 | import { IndentCodeIsListfirstChild } from '../../indentCodeIsListfirstChild'; 5 | import { findOrderListIndentNumber } from '../../findOrderListIndentNumber'; 6 | 7 | export class ListNode { 8 | node: Node; 9 | 10 | constructor(node: Node) { 11 | this.node = node; 12 | } 13 | 14 | private get parent() { 15 | return this.node.parentNode; 16 | } 17 | 18 | get parentIsOL() { 19 | return this.parent && this.parent.nodeName === 'OL'; 20 | } 21 | 22 | get nestULCount() { 23 | return findParentNumber(this.node, 'UL'); 24 | } 25 | 26 | get nestOLCount() { 27 | return findParentNumber(this.node, 'OL'); 28 | } 29 | 30 | get nestCount() { 31 | return this.nestULCount + this.nestOLCount; 32 | } 33 | 34 | get isLoose() { 35 | const node = this.node; 36 | return node.firstChild && node.firstChild.nodeName === 'P'; // Todo:isBlock 37 | } 38 | 39 | get isNewList() { 40 | const parent = this.parent; 41 | return ( 42 | parent && 43 | parent.previousSibling && 44 | parent.previousSibling.nodeName === parent.nodeName 45 | ); 46 | } 47 | 48 | get followCode() { 49 | const parent = this.parent; 50 | return ( 51 | parent && parent.nextSibling && parent.nextSibling.nodeName === 'PRE' 52 | ); 53 | } 54 | 55 | get inLast() { 56 | const parent = this.parent; 57 | return parent && parent.lastChild === this.node; 58 | } 59 | 60 | get nestListAndParentIsEmpty() { 61 | const { nestOLCount, nestULCount, node } = this; 62 | return ( 63 | nestOLCount + nestULCount > 1 && 64 | node.parentNode && 65 | node.parentNode.parentNode && 66 | (node.parentNode.parentNode as HTMLElement).innerHTML === 67 | (node.parentNode as HTMLElement).outerHTML 68 | ); 69 | } 70 | 71 | lineIndent(options: Options) { 72 | const { nestOLCount, nestULCount, nestCount, node } = this; 73 | var indent = `\n ${repeat(' ', nestCount - 1)}$1`; 74 | if (IndentCodeIsListfirstChild(node, options) && nestOLCount) { 75 | indent = `\n ${repeat(' ', nestCount)}$1`; 76 | } else if (nestULCount) { 77 | indent = `\n${repeat(' ', nestCount * 2)}$1`; 78 | } 79 | return indent; 80 | } 81 | 82 | caclPrefix(input: string) { 83 | let prefix = input; 84 | const { 85 | nestOLCount, 86 | nestULCount, 87 | parentIsOL, 88 | node, 89 | parent, 90 | isNewList, 91 | inLast, 92 | isLoose, 93 | followCode, 94 | nestListAndParentIsEmpty, 95 | } = this; 96 | if (parent && parentIsOL) { 97 | var start = (parent as HTMLElement).getAttribute('start'); 98 | var index = Array.prototype.indexOf.call(parent.children, node); 99 | prefix = 100 | (start ? Number(start) + index : index + 1) + 101 | (isNewList ? ') ' : '. '); 102 | } 103 | if (followCode) { 104 | if (!isLoose) prefix = ' ' + prefix + ' '; // example 235 105 | if (inLast && isLoose) { 106 | // example 293 107 | prefix = ' ' + prefix; 108 | } 109 | } 110 | 111 | if (nestULCount > 1) { 112 | prefix = repeat(' ', (nestULCount - 1) * 2) + prefix; 113 | } 114 | if (nestULCount && nestOLCount) { 115 | const indent = findOrderListIndentNumber(node); 116 | prefix = repeat(' ', nestULCount * 4 + indent) + prefix; 117 | } 118 | // Info:嵌套列表且父列表为空 119 | if (nestListAndParentIsEmpty) { 120 | prefix = prefix.trimStart(); 121 | } 122 | 123 | return prefix; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /packages/@sitdown/wechat/test/spec/markdownPaper/paper4.md: -------------------------------------------------------------------------------- 1 | # 解决了!微信公众号数学公式排版 2 | 3 | 解决了!微信公众号数学公式排版难的问题被 `Markdown Nice` 解决了!**全网第一个解决的**!怎么还没人知道呢! 4 | 5 | > **https://mdnice.com/** 给你最完美的解决方案! 6 | 7 | ![](https://my-wechat.mdnice.com/wechat/image_20191013231418.png) 8 | 9 | ![](https://my-wechat.mdnice.com/wechat/image_20191014001842.png) 10 | 11 | ## 行间公式 12 | 13 | 我是清晰的行间公式~ 14 | 15 | $$ 16 | \begin {aligned} c (\theta_{1}) C (\theta_{2}) =&(\cos \theta_{1}+i 17 | \sin \theta_{1})\left (\cos \theta_{2}+i \sin \theta_{2}\right) \\ 18 | =&\left\{\left (\cos \theta_{1} \cos \theta_{2}-\sin \theta_{1} \sin 19 | \theta_{2}\right)\right.\\ &i\left (\cos \theta_{1} \sin 20 | \theta_{2}+\cos \theta_{2} \sin \theta_{1}\right)\} \\ =&\cos \left 21 | (\theta_{1}+\theta_{2}\right)+i \sin \left (\theta_{1}+\theta_{2}\right) 22 | \\ =&c\left (\theta_{1}+\theta_{2}\right) \end {aligned}$$ 23 | 24 | 我是清晰的行间公式~ 25 | 26 | $$ 27 | \left|z_{1}+z_{2}+\cdots+z_{n}\right| \leqslant\left|z_{1}\right|+\left|z_{2}\right|+\cdots+\left|z_{n}\right|.$$ 28 | 29 | 我是清晰的行间公式~ 30 | 31 | $$ 32 | \begin{pmatrix} 33 | 1 & a_1 & a_1^2 & \cdots & a_1^n \\ 34 | 1 & a_2 & a_2^2 & \cdots & a_2^n \\ 35 | \vdots & \vdots & \vdots & \ddots & \vdots \\ 36 | 1 & a_m & a_m^2 & \cdots & a_m^n \\ 37 | \end{pmatrix} 38 | $$ 39 | 40 | 我是清晰的行间公式~ 41 | 42 | ## 行内公式 43 | 44 | 我是整齐的行内公式~ 45 | 46 | 设 $A'$,$B'$ 二点联线段的中点为 $M$, 作 $PP'$,$A^{\prime} A^{\prime \prime}, B^{\prime} B^{\prime \prime}, M A^{\prime}$ 垂直于 $A,B$ 47 | 二点的联线 $l$ 由于 $M$ 是梯形 $A^{\prime} A^{\prime \prime} B^{\prime \prime} B^{\prime}$ 一个腰的 $A'$ 中点,故 $M'$ 必是另一腰 $A''B''$ 的中点。 48 | 49 | 我是整齐的行内公式~ 50 | 51 | 由于 $M'$ 是 $A^{\prime \prime}B^{\prime \prime}$ 的中点,而 $\overline {A A^{\prime \prime}}=\overline 52 | {B B^{\prime \prime}}\left (=P P^{\prime}\right)$, 故 $M'$ 必定同时也是 $AB$ 的中点。 53 | 54 | 我是整齐的行内公式~ 55 | 56 | ## 使用方法 57 | 58 | 忘记那把公式转成图片插入公众号的解决方案吧! 59 | 60 | 使用 `Markdown Nice` 结合 `LaTeX` 语法编写数学公式,一键复制到公众号中,给你飞一般的体验! 61 | 62 | ![Markdown Nice 预览图](https://my-wechat.mdnice.com/wechat/image_20191013233758.png) 63 | 64 | 在左边的编辑框写入 `Markdown` 的内容,数学公式采用 `LaTeX` 语法嵌入其中: 65 | 66 | 1、**行内公式**:将公式前后各加 1 个 `$` 符号,中间插入 `LaTeX` 语法。 67 | 68 | ```tex 69 | 一般情况下 $a+b$ 的结果 70 | ``` 71 | 72 | 2、**行间公式**:将公式前后各加 2 个 `$` 符号,中间插入 `LaTeX` 语法。 73 | 74 | ```tex 75 | $$ 76 | \begin{pmatrix} 77 | 1 & a_1 & a_1^2 & \cdots & a_1^n \\ 78 | 1 & a_2 & a_2^2 & \cdots & a_2^n \\ 79 | \vdots & \vdots & \vdots & \ddots & \vdots \\ 80 | 1 & a_m & a_m^2 & \cdots & a_m^n \\ 81 | \end{pmatrix} 82 | $$ 83 | ``` 84 | 85 | 编辑器是实时渲染的,在右边可以实时看到效果。编辑完文章后点击上方工具栏的**蓝色复制按钮**,等出现下图提示: 86 | 87 | ![复制成功提示](https://my-wechat.mdnice.com/wechat/image_20191013234050.png) 88 | 89 | 就可以直接在微信公众号后台编辑器直接 `Ctrl + v` 粘贴内容了。 90 | 91 | ![微信编辑器效果](https://my-wechat.mdnice.com/wechat/image_20191013234234.png) 92 | 93 | 如果对 `LaTeX` 语法不熟悉,可参考知乎问题:[知乎上的公式是怎么打出来的?](https://www.zhihu.com/question/31298277 "知乎上的公式是怎么打出来的?")。为了优化微信公众号显示,LaTeX 公式书写建议: 94 | 95 | 1. `\tag{xxx}` 改为 `\qquad (xxx)`(避免公式被缩小) 96 | 2. 长的行内公式改为行间公式(优化断行),并适当换行。不要直接使用 `\\` 来换行,要使用 `aligned` 等对其环境。(避免公式被缩小) 97 | 98 | **需要注意的是**:本工具排版的公式在公众号后台二次编辑的时候很容易丢失,所以尽量避免该操作。 99 | 100 | ![哇](https://my-wechat.mdnice.com/wechat/image_20191014001401.png) 101 | 102 | 这么整齐的公式!这么细致的提示!是不是已经被感动到了!还不快来试一下! 103 | 104 | > 访问 **https://mdnice.com/** 极速体验! 105 | 106 | 感谢`@Phoebe(创始人)`、`@idx(公式终结者)`、`@云影(图床和组件化大佬)`这几位主要开发者提供的技术支持,在开源世界中始终以提升用户体验为目标而努力。 107 | 108 | 感谢所有主题设计者为用户提供优质选择。 109 | 110 | 感谢所有用户为开发者提供了诸多宝贵意见。 111 | 112 | 关注公众号回复「排版」加入用户群 113 | 觉得公式直击灵魂,欢迎点击在看转发 114 | 115 | ![](http://draw-wechat.oss-cn-hangzhou.aliyuncs.com/%E4%BA%8C%E7%BB%B4%E7%A0%81_20190823124950.gif) -------------------------------------------------------------------------------- /packages/core/src/util/collapse-whitespace.ts: -------------------------------------------------------------------------------- 1 | import isBlock from './isBlock'; 2 | import isVoid from './isVoid'; 3 | import { Node } from '../types'; 4 | 5 | interface Options { 6 | element: Node; 7 | isBlock: typeof isBlock; 8 | isVoid: typeof isVoid; 9 | isPre?: (node: Node) => boolean; 10 | } 11 | 12 | /** 13 | * collapseWhitespace(options) removes extraneous whitespace from an the given element. 14 | * 15 | * @param {Object} options 16 | */ 17 | function collapseWhitespace(options: Options) { 18 | var element = options.element; 19 | var isBlock = options.isBlock; 20 | var isVoid = options.isVoid; 21 | var isPre = 22 | options.isPre || 23 | function(node: any) { 24 | return node.nodeName === 'PRE'; 25 | }; 26 | 27 | if (!element.firstChild || isPre(element)) return; 28 | 29 | var prevText = null; 30 | var prevVoid = false; 31 | 32 | var prev = null; 33 | var node = next(prev, element, isPre); 34 | 35 | while (node !== element) { 36 | if (node.nodeType === 3 || node.nodeType === 4) { 37 | // Node.TEXT_NODE or Node.CDATA_SECTION_NODE 38 | var text = node.data ? node.data.replace(/[ \r\n\t]+/g, ' ') : ''; 39 | if ( 40 | (!prevText || !prevText.data || / $/.test(prevText.data)) && 41 | !prevVoid && 42 | text[0] === ' ' 43 | ) { 44 | text = text.substr(1); 45 | } 46 | 47 | // `text` might be empty at this point. 48 | if (!text) { 49 | node = remove(node); 50 | continue; 51 | } 52 | 53 | node.data = text; 54 | 55 | prevText = node; 56 | } else if (node.nodeType === 1) { 57 | // Node.ELEMENT_NODE 58 | if (isBlock(node) || node.nodeName === 'BR') { 59 | if (prevText && prevText.data) { 60 | prevText.data = prevText.data.replace(/ $/, ''); 61 | } 62 | 63 | prevText = null; 64 | prevVoid = false; 65 | } else if (isVoid(node)) { 66 | // Avoid trimming space around non-block, non-BR void elements. 67 | prevText = null; 68 | prevVoid = true; 69 | } 70 | } else if (node.nodeType === 8) { 71 | if (node.nextElementSibling && node.parentNode) { 72 | const index = Array.from(node.parentNode.childNodes).findIndex( 73 | n => n === node 74 | ); 75 | (node.parentNode.childNodes[index + 1] as Node).unNeedEscape = true; 76 | } 77 | } else { 78 | node = remove(node); 79 | continue; 80 | } 81 | 82 | var nextNode = next(prev, node, isPre); 83 | prev = node; 84 | node = nextNode; 85 | } 86 | 87 | if (prevText && prevText.data) { 88 | prevText.data = prevText.data.replace(/ $/, ''); 89 | if (!prevText.data) { 90 | remove(prevText); 91 | } 92 | } 93 | } 94 | 95 | /** 96 | * remove(node) removes the given node from the DOM and returns the 97 | * next node in the sequence. 98 | * 99 | * @param {Node} node 100 | * @return {Node} node 101 | */ 102 | function remove(node: Node) { 103 | var next = node.nextSibling || node.parentNode; 104 | 105 | node.parentNode && node.parentNode.removeChild(node); 106 | 107 | return next as Node; 108 | } 109 | 110 | /** 111 | * next(prev, current, isPre) returns the next node in the sequence, given the 112 | * current and previous nodes. 113 | * 114 | * @param {Node} prev 115 | * @param {Node} current 116 | * @param {Function} isPre 117 | * @return {Node} 118 | */ 119 | function next( 120 | prev: Node | null, 121 | current: Node, 122 | isPre: (node: Node) => boolean 123 | ) { 124 | if ((prev && prev.parentNode === current) || isPre(current)) { 125 | return (current.nextSibling || current.parentNode) as Node; 126 | } 127 | 128 | return (current.firstChild || 129 | current.nextSibling || 130 | current.parentNode) as Node; 131 | } 132 | 133 | export default collapseWhitespace; 134 | -------------------------------------------------------------------------------- /packages/core/test/spec/md-it-plugin-taskList.js: -------------------------------------------------------------------------------- 1 | // Markdown-it plugin to render GitHub-style task lists; see 2 | // 3 | // https://github.com/blog/1375-task-lists-in-gfm-issues-pulls-comments 4 | // https://github.com/blog/1825-task-lists-in-all-markdown-documents 5 | 6 | var disableCheckboxes = true; 7 | var useLabelWrapper = false; 8 | var useLabelAfter = false; 9 | 10 | module.exports = function(md, options) { 11 | if (options) { 12 | disableCheckboxes = !options.enabled; 13 | useLabelWrapper = !!options.label; 14 | useLabelAfter = !!options.labelAfter; 15 | } 16 | 17 | md.core.ruler.after('inline', 'github-task-lists', function(state) { 18 | var tokens = state.tokens; 19 | for (var i = 2; i < tokens.length; i++) { 20 | if (isTodoItem(tokens, i)) { 21 | todoify(tokens[i], state.Token); 22 | // attrSet(tokens[i-2], 'class', 'task-list-item' + (!disableCheckboxes ? ' enabled' : '')); 23 | // attrSet(tokens[parentToken(tokens, i-2)], 'class', 'contains-task-list'); 24 | } 25 | } 26 | }); 27 | }; 28 | 29 | function attrSet(token, name, value) { 30 | var index = token.attrIndex(name); 31 | var attr = [name, value]; 32 | 33 | if (index < 0) { 34 | token.attrPush(attr); 35 | } else { 36 | token.attrs[index] = attr; 37 | } 38 | } 39 | 40 | function parentToken(tokens, index) { 41 | var targetLevel = tokens[index].level - 1; 42 | for (var i = index - 1; i >= 0; i--) { 43 | if (tokens[i].level === targetLevel) { 44 | return i; 45 | } 46 | } 47 | return -1; 48 | } 49 | 50 | function isTodoItem(tokens, index) { 51 | return isInline(tokens[index]) && 52 | isParagraph(tokens[index - 1]) && 53 | isListItem(tokens[index - 2]) && 54 | startsWithTodoMarkdown(tokens[index]); 55 | } 56 | 57 | function todoify(token, TokenConstructor) { 58 | token.children.unshift(makeCheckbox(token, TokenConstructor)); 59 | token.children[1].content = token.children[1].content.slice(3); 60 | token.content = token.content.slice(3); 61 | 62 | if (useLabelWrapper) { 63 | if (useLabelAfter) { 64 | token.children.pop(); 65 | 66 | // Use large random number as id property of the checkbox. 67 | var id = 'task-item-' + Math.ceil(Math.random() * (10000 * 1000) - 1000); 68 | token.children[0].content = token.children[0].content.slice(0, -1) + ' id="' + id + '">'; 69 | token.children.push(afterLabel(token.content, id, TokenConstructor)); 70 | } else { 71 | token.children.unshift(beginLabel(TokenConstructor)); 72 | token.children.push(endLabel(TokenConstructor)); 73 | } 74 | } 75 | } 76 | 77 | function makeCheckbox(token, TokenConstructor) { 78 | var checkbox = new TokenConstructor('html_inline', '', 0); 79 | var disabledAttr = disableCheckboxes ? ' disabled="" ' : ''; 80 | if (token.content.indexOf('[ ] ') === 0) { 81 | checkbox.content = ''; 82 | } else if (token.content.indexOf('[x] ') === 0 || token.content.indexOf('[X] ') === 0) { 83 | checkbox.content = ''; 84 | } 85 | return checkbox; 86 | } 87 | 88 | // these next two functions are kind of hacky; probably should really be a 89 | // true block-level token with .tag=='label' 90 | function beginLabel(TokenConstructor) { 91 | var token = new TokenConstructor('html_inline', '', 0); 92 | token.content = ''; 99 | return token; 100 | } 101 | 102 | function afterLabel(content, id, TokenConstructor) { 103 | var token = new TokenConstructor('html_inline', '', 0); 104 | token.content = ''; 105 | token.attrs = [{for: id}]; 106 | return token; 107 | } 108 | 109 | function isInline(token) { return token.type === 'inline'; } 110 | function isParagraph(token) { return token.type === 'paragraph_open'; } 111 | function isListItem(token) { return token.type === 'list_item_open'; } 112 | 113 | function startsWithTodoMarkdown(token) { 114 | // leading whitespace in a list item is already trimmed off by markdown-it 115 | return token.content.indexOf('[ ] ') === 0 || token.content.indexOf('[x] ') === 0 || token.content.indexOf('[X] ') === 0; 116 | } -------------------------------------------------------------------------------- /packages/docs/.vuepress/components/Demo.vue: -------------------------------------------------------------------------------- 1 |