├── tests ├── mdStrings │ ├── code-long.confluence │ ├── code-short.confluence │ ├── html.confluence │ ├── bold.md │ ├── code-html.md │ ├── html.md │ ├── image.confluence │ ├── italics.md │ ├── link.confluence │ ├── bold.confluence │ ├── link.md │ ├── plaintext.confluence │ ├── plaintext.md │ ├── strikethrough.confluence │ ├── strikethrough.md │ ├── hr.confluence │ ├── imageReference.confluence │ ├── italics.confluence │ ├── linkReference.confluence │ ├── image.md │ ├── paragraph.confluence │ ├── hr.md │ ├── paragraph.md │ ├── code-fence.md │ ├── code-indentation.md │ ├── code-html.confluence │ ├── code-languagemap.md │ ├── code-not-collapsing.md │ ├── codespan-brackets.md │ ├── table.confluence │ ├── br.confluence │ ├── br.md │ ├── codespan-brackets.confluence │ ├── linkReference.md │ ├── headings.md │ ├── imageReference.md │ ├── headings.confluence │ ├── blockquote.md │ ├── list-unordered-expanded.md │ ├── list-ordered-expanded.md │ ├── list-unordered-collapsed.md │ ├── list-ordered-collapsed.md │ ├── table.md │ ├── list-ordered-collapsed.confluence │ ├── list-ordered-expanded.confluence │ ├── list-unordered-expanded.confluence │ ├── blockquote.confluence │ ├── list-unordered-collapsed.confluence │ ├── blockquoteMulti.md │ ├── code-fence.confluence │ ├── code-languagemap.confluence │ ├── code-indentation.confluence │ ├── code-not-collapsing.confluence │ ├── blockquoteMulti.confluence │ ├── list-complex.confluence │ ├── list-complex.md │ ├── code-short.md │ └── code-long.md ├── utils │ ├── TestSaver.js │ └── testStringLoader.js ├── blockquote.test.js ├── link.test.js ├── image.test.js ├── list.test.js ├── test.md ├── simple.test.js └── code.test.js ├── .gitignore ├── .babelrc ├── .travis.yml ├── defaultLanguageMap.json ├── demo └── demo.md ├── .eslintrc ├── bin └── markdown2confluence.js ├── .github └── workflows │ └── npm-publish.yml ├── package.json ├── README.md └── index.js /tests/mdStrings/code-long.confluence: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/mdStrings/code-short.confluence: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/mdStrings/html.confluence: -------------------------------------------------------------------------------- 1 | div. \n -------------------------------------------------------------------------------- /tests/mdStrings/bold.md: -------------------------------------------------------------------------------- 1 | **one** and __two__ -------------------------------------------------------------------------------- /tests/mdStrings/code-html.md: -------------------------------------------------------------------------------- 1 | `it&s` 2 | -------------------------------------------------------------------------------- /tests/mdStrings/html.md: -------------------------------------------------------------------------------- 1 |
\n
2 | -------------------------------------------------------------------------------- /tests/mdStrings/image.confluence: -------------------------------------------------------------------------------- 1 | !image.png! -------------------------------------------------------------------------------- /tests/mdStrings/italics.md: -------------------------------------------------------------------------------- 1 | *one* and _two_ -------------------------------------------------------------------------------- /tests/mdStrings/link.confluence: -------------------------------------------------------------------------------- 1 | [text|url/] -------------------------------------------------------------------------------- /tests/mdStrings/bold.confluence: -------------------------------------------------------------------------------- 1 | *one* and *two* -------------------------------------------------------------------------------- /tests/mdStrings/link.md: -------------------------------------------------------------------------------- 1 | [text](url/ 'title') 2 | -------------------------------------------------------------------------------- /tests/mdStrings/plaintext.confluence: -------------------------------------------------------------------------------- 1 | text\ntext2 -------------------------------------------------------------------------------- /tests/mdStrings/plaintext.md: -------------------------------------------------------------------------------- 1 | text\ntext2 2 | -------------------------------------------------------------------------------- /tests/mdStrings/strikethrough.confluence: -------------------------------------------------------------------------------- 1 | -thing- -------------------------------------------------------------------------------- /tests/mdStrings/strikethrough.md: -------------------------------------------------------------------------------- 1 | ~~thing~~ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | coverage/* -------------------------------------------------------------------------------- /tests/mdStrings/hr.confluence: -------------------------------------------------------------------------------- 1 | ---- 2 | 3 | Words here -------------------------------------------------------------------------------- /tests/mdStrings/imageReference.confluence: -------------------------------------------------------------------------------- 1 | !image.png! -------------------------------------------------------------------------------- /tests/mdStrings/italics.confluence: -------------------------------------------------------------------------------- 1 | _one_ and _two_ -------------------------------------------------------------------------------- /tests/mdStrings/linkReference.confluence: -------------------------------------------------------------------------------- 1 | [text|url/] -------------------------------------------------------------------------------- /tests/mdStrings/image.md: -------------------------------------------------------------------------------- 1 | ![alt text](image.png 'title') -------------------------------------------------------------------------------- /tests/mdStrings/paragraph.confluence: -------------------------------------------------------------------------------- 1 | line1 2 | 3 | line2 -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /tests/mdStrings/hr.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | -------------- 4 | Words here -------------------------------------------------------------------------------- /tests/mdStrings/paragraph.md: -------------------------------------------------------------------------------- 1 | line1 2 | 3 | 4 | 5 | line2 6 | -------------------------------------------------------------------------------- /tests/mdStrings/code-fence.md: -------------------------------------------------------------------------------- 1 | ```js 2 | this is code 3 | ``` 4 | -------------------------------------------------------------------------------- /tests/mdStrings/code-indentation.md: -------------------------------------------------------------------------------- 1 | 2 | // different code 3 | -------------------------------------------------------------------------------- /tests/mdStrings/code-html.confluence: -------------------------------------------------------------------------------- 1 | {{it&#38;s}} 2 | 3 | -------------------------------------------------------------------------------- /tests/mdStrings/code-languagemap.md: -------------------------------------------------------------------------------- 1 | ```leet 2 | this is code 3 | ``` 4 | -------------------------------------------------------------------------------- /tests/mdStrings/code-not-collapsing.md: -------------------------------------------------------------------------------- 1 | ```js 2 | this is code 3 | ``` 4 | -------------------------------------------------------------------------------- /tests/mdStrings/codespan-brackets.md: -------------------------------------------------------------------------------- 1 | text `code` text text `code` 2 | -------------------------------------------------------------------------------- /tests/mdStrings/table.confluence: -------------------------------------------------------------------------------- 1 | ||heading||heading2|| 2 | |cell1|cell2| -------------------------------------------------------------------------------- /tests/mdStrings/br.confluence: -------------------------------------------------------------------------------- 1 | Some simple text 2 | 3 | 4 | Some other text -------------------------------------------------------------------------------- /tests/mdStrings/br.md: -------------------------------------------------------------------------------- 1 | Some simple text\ 2 | \ 3 | \ 4 | Some other text 5 | -------------------------------------------------------------------------------- /tests/mdStrings/codespan-brackets.confluence: -------------------------------------------------------------------------------- 1 | text {{code}} text text {{code}} -------------------------------------------------------------------------------- /tests/mdStrings/linkReference.md: -------------------------------------------------------------------------------- 1 | [text][ref] 2 | 3 | [ref]: (title) -------------------------------------------------------------------------------- /tests/mdStrings/headings.md: -------------------------------------------------------------------------------- 1 | # multi-line heading 2 | 3 | ###### single-line heading 4 | -------------------------------------------------------------------------------- /tests/mdStrings/imageReference.md: -------------------------------------------------------------------------------- 1 | ![alt text][img] 2 | 3 | [img]: image.png 'title' 4 | -------------------------------------------------------------------------------- /tests/mdStrings/headings.confluence: -------------------------------------------------------------------------------- 1 | h1. multi-line heading 2 | 3 | h6. single-line heading -------------------------------------------------------------------------------- /tests/mdStrings/blockquote.md: -------------------------------------------------------------------------------- 1 | Paragraph 2 | 3 | > one line quote 4 | 5 | Another paragraph 6 | -------------------------------------------------------------------------------- /tests/mdStrings/list-unordered-expanded.md: -------------------------------------------------------------------------------- 1 | Unordered, expanded 2 | 3 | * one 4 | * two 5 | * three -------------------------------------------------------------------------------- /tests/mdStrings/list-ordered-expanded.md: -------------------------------------------------------------------------------- 1 | Ordered, collapsed 2 | 3 | 1. one 4 | 2. two 5 | 3. three 6 | -------------------------------------------------------------------------------- /tests/mdStrings/list-unordered-collapsed.md: -------------------------------------------------------------------------------- 1 | Unordered, collapsed 2 | 3 | * one 4 | * two 5 | * three -------------------------------------------------------------------------------- /tests/mdStrings/list-ordered-collapsed.md: -------------------------------------------------------------------------------- 1 | Ordered, collapsed 2 | 3 | 1. one 4 | 2. two 5 | 3. three 6 | -------------------------------------------------------------------------------- /tests/mdStrings/table.md: -------------------------------------------------------------------------------- 1 | | heading | heading2 | 2 | | ------- | -------- | 3 | | cell1 | cell2 | 4 | -------------------------------------------------------------------------------- /tests/mdStrings/list-ordered-collapsed.confluence: -------------------------------------------------------------------------------- 1 | Ordered, collapsed 2 | 3 | # one 4 | # two 5 | # three 6 | 7 | -------------------------------------------------------------------------------- /tests/mdStrings/list-ordered-expanded.confluence: -------------------------------------------------------------------------------- 1 | Ordered, collapsed 2 | 3 | # one 4 | # two 5 | # three 6 | 7 | -------------------------------------------------------------------------------- /tests/mdStrings/list-unordered-expanded.confluence: -------------------------------------------------------------------------------- 1 | Unordered, expanded 2 | 3 | * one 4 | * two 5 | * three 6 | 7 | -------------------------------------------------------------------------------- /tests/mdStrings/blockquote.confluence: -------------------------------------------------------------------------------- 1 | Paragraph 2 | 3 | {quote} 4 | one line quote 5 | {quote} 6 | 7 | Another paragraph -------------------------------------------------------------------------------- /tests/mdStrings/list-unordered-collapsed.confluence: -------------------------------------------------------------------------------- 1 | Unordered, collapsed 2 | 3 | * one 4 | * two 5 | * three 6 | 7 | -------------------------------------------------------------------------------- /tests/mdStrings/blockquoteMulti.md: -------------------------------------------------------------------------------- 1 | > line 1 2 | > line 2 3 | 4 | inner text 5 | 6 | > quote 2 7 | > More quote 2 8 | > and more 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '14' 4 | before_install: 5 | - npm install -g npm@latest 6 | - npm install -g codecov 7 | -------------------------------------------------------------------------------- /tests/mdStrings/code-fence.confluence: -------------------------------------------------------------------------------- 1 | {code:title=none|language=javascript|borderStyle=solid|theme=RDark|linenumbers=true|collapse=true} 2 | this is code 3 | {code} -------------------------------------------------------------------------------- /tests/mdStrings/code-languagemap.confluence: -------------------------------------------------------------------------------- 1 | {code:title=none|language=1337|borderStyle=solid|theme=RDark|linenumbers=true|collapse=true} 2 | this is code 3 | {code} -------------------------------------------------------------------------------- /tests/mdStrings/code-indentation.confluence: -------------------------------------------------------------------------------- 1 | {code:title=none|language=none|borderStyle=solid|theme=RDark|linenumbers=true|collapse=true} 2 | // different code 3 | {code} -------------------------------------------------------------------------------- /tests/mdStrings/code-not-collapsing.confluence: -------------------------------------------------------------------------------- 1 | {code:title=none|language=javascript|borderStyle=solid|theme=RDark|linenumbers=true|collapse=false} 2 | this is code 3 | {code} -------------------------------------------------------------------------------- /tests/mdStrings/blockquoteMulti.confluence: -------------------------------------------------------------------------------- 1 | {quote} 2 | line 1 3 | line 2 4 | {quote} 5 | 6 | inner text 7 | 8 | {quote} 9 | quote 2 10 | More quote 2 11 | and more 12 | {quote} -------------------------------------------------------------------------------- /tests/utils/TestSaver.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const testsFolder = `${process.cwd()}/tests/mdStrings/`; 4 | fs.writeFileSync( 5 | path.resolve(testsFolder, `list-ordered-expanded.confluence`), 6 | convert(source), 7 | ); 8 | -------------------------------------------------------------------------------- /tests/mdStrings/list-complex.confluence: -------------------------------------------------------------------------------- 1 | * unordered:1:1 2 | * unordered:1:2 3 | *# ordered:2:1 4 | *## ordered:3:1 5 | *## ordered:3:2 6 | *# ordered:2:2 7 | *#* unordered:4:1 8 | *#* unordered:4:2 9 | *# ordered:2:3 10 | *#* unordered:5:1 11 | * unordered:1:3 12 | ** unordered:6:1 13 | **# ordered:7:1 14 | **# ordered:7:2 15 | ** unordered:6:2 16 | *** unordered:8:1 17 | *** unordered:8:2 18 | * unordered:1:4 19 | 20 | -------------------------------------------------------------------------------- /tests/mdStrings/list-complex.md: -------------------------------------------------------------------------------- 1 | * unordered:1:1 2 | * unordered:1:2 3 | 1. ordered:2:1 4 | 1. ordered:3:1 5 | 2. ordered:3:2 6 | 2. ordered:2:2 7 | * unordered:4:1 8 | * unordered:4:2 9 | 3. ordered:2:3 10 | * unordered:5:1 11 | * unordered:1:3 12 | * unordered:6:1 13 | 1. ordered:7:1 14 | 2. ordered:7:2 15 | * unordered:6:2 16 | * unordered:8:1 17 | * unordered:8:2 18 | * unordered:1:4 -------------------------------------------------------------------------------- /tests/utils/testStringLoader.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const testsFolder = `${process.cwd()}/tests/mdStrings/`; 5 | 6 | const getTestStrings = (testName) => { 7 | const source = fs 8 | .readFileSync(path.resolve(testsFolder, `${testName}.md`)) 9 | .toString(); 10 | 11 | const target = fs 12 | .readFileSync(path.resolve(testsFolder, `${testName}.confluence`)) 13 | .toString(); 14 | 15 | return {source, target}; 16 | }; 17 | 18 | module.exports = getTestStrings; 19 | export default getTestStrings; 20 | -------------------------------------------------------------------------------- /tests/mdStrings/code-short.md: -------------------------------------------------------------------------------- 1 | ```JavaScript 2 | const convert = require('..'); 3 | const getTestStrings = require('./utils/testStringLoader'); 4 | 5 | describe('Code tests', () => { 6 | it('Format with code fences', () => { 7 | const {source, target} = getTestStrings('code-fence'); 8 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 9 | }); 10 | 11 | it('Format with code indentation', () => { 12 | const {source, target} = getTestStrings('code-indentation'); 13 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 14 | }); 15 | }); 16 | ``` 17 | -------------------------------------------------------------------------------- /tests/blockquote.test.js: -------------------------------------------------------------------------------- 1 | const convert = require('..'); 2 | const getTestStrings = require('./utils/testStringLoader'); 3 | 4 | describe('Blockquote tests', () => { 5 | it('converts a single line quote correctly', () => { 6 | const {source, target} = getTestStrings('blockquote'); 7 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 8 | }); 9 | 10 | it('works on multi-line quotes and multiple quotes', () => { 11 | const {source, target} = getTestStrings('blockquoteMulti'); 12 | 13 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /defaultLanguageMap.json: -------------------------------------------------------------------------------- 1 | { 2 | "": "none", 3 | "actionscript3": "actionscript3", 4 | "bash": "bash", 5 | "csharp": "csharp", 6 | "coldfusion": "coldfusion", 7 | "cpp": "cpp", 8 | "css": "css", 9 | "delphi": "delphi", 10 | "diff": "diff", 11 | "erlang": "erlang", 12 | "groovy": "groovy", 13 | "html": "html", 14 | "java": "java", 15 | "javafx": "javafx", 16 | "javascript": "javascript", 17 | "js": "javascript", 18 | "perl": "perl", 19 | "php": "php", 20 | "powershell": "powershell", 21 | "python": "python", 22 | "ruby": "ruby", 23 | "scala": "scala", 24 | "shell": "bash", 25 | "sql": "sql", 26 | "vb": "vb", 27 | "xml": "xml" 28 | } 29 | -------------------------------------------------------------------------------- /demo/demo.md: -------------------------------------------------------------------------------- 1 | # h1 2 | 3 | # head1 4 | 5 | ## head2 6 | 7 | ### head3 8 | 9 | - **strong** 10 | - _emphasis_ 11 | - ~~del~~ 12 | - `code inline` 13 | 14 | > block quote 15 | 16 | [github link address](https://github.com/Shogobg/markdown2confluence/) 17 | 18 | ```javascript 19 | var i = 1; // comment 20 | console.log('This is code block'); 21 | ``` 22 | 23 | ![image](https://www.google.com.hk/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png) 24 | 25 | ## GFM support 26 | 27 | | First Header | Second Header | 28 | | -------------- | ---------------- | 29 | | Content Cell | Content Cell | 30 | | Content Cell | Content Cell | 31 | | _inline style_ | **inline style** | 32 | 33 | :) 34 | -------------------------------------------------------------------------------- /tests/link.test.js: -------------------------------------------------------------------------------- 1 | const convert = require('..'); 2 | const getTestStrings = require('./utils/testStringLoader'); 3 | 4 | describe('Link tests', () => { 5 | it('Simple link', () => { 6 | const {source, target} = getTestStrings('link'); 7 | 8 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 9 | }); 10 | 11 | it('Link with a reference definition', () => { 12 | const {source, target} = getTestStrings('linkReference'); 13 | 14 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 15 | }); 16 | 17 | it('Change anchor URL renderer', () => { 18 | expect( 19 | convert('[text](url/)', { 20 | renderer: { 21 | link: (href) => { 22 | return `http://example.com/${href}`; 23 | }, 24 | }, 25 | }), 26 | ).toBe('http://example.com/url/\n\n'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/image.test.js: -------------------------------------------------------------------------------- 1 | const convert = require('..'); 2 | const getTestStrings = require('./utils/testStringLoader'); 3 | 4 | describe('Image tests', () => { 5 | it('Simple image', () => { 6 | const {source, target} = getTestStrings('image'); 7 | 8 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 9 | }); 10 | 11 | it('Image with referenced link', () => { 12 | const {source, target} = getTestStrings('imageReference'); 13 | 14 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 15 | }); 16 | 17 | it('Change image URL renderer', () => { 18 | expect( 19 | convert('![alt text](image.png)', { 20 | renderer: { 21 | image: (href) => { 22 | return `http://example.com/${href}`; 23 | }, 24 | }, 25 | }), 26 | ).toBe('http://example.com/image.png\n\n'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaFeatures": { 5 | "classes": true 6 | } 7 | }, 8 | "env": { 9 | "browser": true, 10 | "commonjs": true, 11 | "es6": true, 12 | "node": true 13 | }, 14 | "plugins": ["prettier", "import"], 15 | "extends": ["prettier"], 16 | "rules": { 17 | "max-len": ["error", {"ignoreComments": true}], 18 | "prettier/prettier": [ 19 | "error", 20 | { 21 | "allowParens": "avoid", 22 | "singleQuote": true, 23 | "trailingComma": "all", 24 | "bracketSpacing": false 25 | } 26 | ], 27 | "import/no-unresolved": [ 28 | "error", 29 | { 30 | "ignore": ["react"] 31 | } 32 | ], 33 | "strict": 0, 34 | "no-restricted-syntax": 0, 35 | "no-use-before-define": 0, 36 | "one-var": 0, 37 | "import/prefer-default-export": 0, 38 | "import/extensions": 0, 39 | "import/no-extraneous-dependencies": 0 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /bin/markdown2confluence.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var md2conflu = require('../'); 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var assert = require('assert'); 6 | 7 | var filename = process.argv[2]; 8 | const outputFileName = process.argv[3]; 9 | 10 | if (!filename) { 11 | filename = '/dev/stdin'; 12 | } 13 | 14 | fs.readFile(path.resolve(process.cwd(), filename), (err, buffer) => { 15 | assert(!err, 'read file ' + filename + ' error!'); 16 | 17 | // We want to remove widdershins metadata 18 | let commentEnd = buffer.indexOf('-->', 0); 19 | 20 | commentEnd = commentEnd > 0 ? (commentEnd += 3) : 0; 21 | 22 | // Generate the confluence wiki markup 23 | const confluenceMarkup = md2conflu(buffer.slice(commentEnd, buffer.length)); 24 | 25 | if (outputFileName) { 26 | fs.writeFileSync( 27 | path.resolve(process.cwd(), outputFileName), 28 | confluenceMarkup, 29 | ); 30 | } else { 31 | console.log(confluenceMarkup); 32 | } 33 | 34 | return confluenceMarkup; 35 | }); 36 | -------------------------------------------------------------------------------- /tests/mdStrings/code-long.md: -------------------------------------------------------------------------------- 1 | ```JavaScript 2 | const convert = require('..'); 3 | const getTestStrings = require('./utils/testStringLoader'); 4 | 5 | describe('Code tests', () => { 6 | it('Format with code fences', () => { 7 | const {source, target} = getTestStrings('code-fence'); 8 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 9 | }); 10 | 11 | it('Format with code indentation', () => { 12 | const {source, target} = getTestStrings('code-indentation'); 13 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 14 | }); 15 | 16 | it('Codespan brackets', () => { 17 | const {source, target} = getTestStrings('codespan-brackets'); 18 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 19 | }); 20 | 21 | it('Code HTML', () => { 22 | // The markdown processing treats this as NOT HTML, so it is 23 | // going to escape the & into & first. 24 | const {source, target} = getTestStrings('code-html'); 25 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 26 | }); 27 | }); 28 | ``` 29 | -------------------------------------------------------------------------------- /tests/list.test.js: -------------------------------------------------------------------------------- 1 | const convert = require('..'); 2 | const getTestStrings = require('./utils/testStringLoader'); 3 | 4 | describe('List tests', () => { 5 | it('Converts an unordered, collapsed list', () => { 6 | const {source, target} = getTestStrings('list-unordered-collapsed'); 7 | 8 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 9 | }); 10 | 11 | it('Converts an unordered, expanded list', () => { 12 | const {source, target} = getTestStrings('list-unordered-expanded'); 13 | 14 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 15 | }); 16 | 17 | it('Converts an ordered, collapsed list', () => { 18 | const {source, target} = getTestStrings('list-ordered-collapsed'); 19 | 20 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 21 | }); 22 | 23 | it('Converts an ordered, expanded list', () => { 24 | const {source, target} = getTestStrings('list-ordered-expanded'); 25 | 26 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 27 | }); 28 | 29 | it('Converts complex list', () => { 30 | const {source, target} = getTestStrings('list-complex'); 31 | 32 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/test.md: -------------------------------------------------------------------------------- 1 | > task items are not converted 2 | 3 | - [ ] Mercury 4 | - [x] Venus 5 | - [x] Earth (Orbit/Moon) 6 | - [x] Mars 7 | - [ ] Jupiter 8 | - [ ] Saturn 9 | - [ ] Uranus 10 | - [ ] Neptune 11 | - [ ] Comet Haley 12 | 13 | > Images are not converted 14 | > Emoji are not converted 15 | 16 | > Markdown input || Expected Confluence output Actual || Confluence output 17 | > `{hello} {world}` || `{{\{hello} \{world}}}` || `{{{hello} {world}}}` 18 | > When the input Markdown contains `{` in a code span, the text is wrapped in `{{` `}}`, but Confluence also expects the inner `{` to be escaped with `\`. This produces a warning when inserted into Confluence: 19 | > 20 | > ``` 21 | > {{ 22 | > Unknown macro: {hello} 23 | > Unknown macro: {world} 24 | > }} 25 | > ``` 26 | 27 | > Link text not converted 28 | > [GitHub Flavored Markdown](https://github.github.com/gfm/) 29 | 30 | > Horizontal line not rendering on a new line 31 | 32 | # Input 33 | 34 | ## Hello 35 | 36 | --- 37 | 38 | ## World 39 | 40 | # Expected 41 | 42 | h1. Hello 43 | 44 | --- 45 | 46 | h1. World 47 | 48 | # Result 49 | 50 | h1. Hello 51 | 52 | ----h1. World 53 | 54 | > Nested list items not working 55 | 56 | - item 57 | - nested 58 | 59 | # expected 60 | 61 | - item 62 | \*\* nested 63 | 64 | # got 65 | 66 | - item\* nested 67 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | # - uses: codecov/codecov-action@v1 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: 20 19 | - run: npm ci 20 | - run: npm test 21 | 22 | publish-npm: 23 | needs: build 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: actions/setup-node@v4 28 | with: 29 | node-version: 20 30 | registry-url: https://registry.npmjs.org/ 31 | - run: npm ci 32 | - run: npm publish --access public 33 | env: 34 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 35 | 36 | # publish-gpr: 37 | # needs: build 38 | # runs-on: ubuntu-latest 39 | # steps: 40 | # - uses: actions/checkout@v2 41 | # - uses: actions/setup-node@v1 42 | # with: 43 | # node-version: 12 44 | # registry-url: https://npm.pkg.github.com/ 45 | # - run: npm ci 46 | # - run: npm publish 47 | # env: 48 | # NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@shogobg/markdown2confluence", 3 | "version": "0.1.11", 4 | "description": "Convert Markdown to Confluence markup", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest --ci --coverage" 8 | }, 9 | "author": "Milen Georgiev ", 10 | "license": "ISC", 11 | "dependencies": { 12 | "marked": "^12.0.2" 13 | }, 14 | "engines": { 15 | "node": ">= 18.0.0" 16 | }, 17 | "bin": { 18 | "markdown2confluence": "bin/markdown2confluence.js" 19 | }, 20 | "devDependencies": { 21 | "@babel/core": "^7.12.3", 22 | "@babel/node": "^7.12.1", 23 | "@babel/preset-env": "^7.12.1", 24 | "babel-eslint": "^10.1.0", 25 | "eslint": "^7.12.1", 26 | "eslint-config-airbnb": "^18.2.0", 27 | "eslint-config-prettier": "^8.1.0", 28 | "eslint-plugin-import": "^2.22.1", 29 | "eslint-plugin-prettier": "^3.1.4", 30 | "jest": "^26.6.2", 31 | "prettier": "^2.1.2", 32 | "webpack": "^5.96.1" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/Shogobg/markdown2confluence.git" 37 | }, 38 | "keywords": [ 39 | "markdown", 40 | "confluence", 41 | "markup", 42 | "convert" 43 | ], 44 | "bugs": { 45 | "url": "https://github.com/Shogobg/markdown2confluence/issues" 46 | }, 47 | "homepage": "https://shogo.eu/", 48 | "directories": { 49 | "test": "tests" 50 | }, 51 | "jest": { 52 | "testEnvironment": "node", 53 | "coveragePathIgnorePatterns": [ 54 | "/node_modules/" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/simple.test.js: -------------------------------------------------------------------------------- 1 | const convert = require('..'); 2 | const getTestStrings = require('./utils/testStringLoader'); 3 | 4 | describe('Simple tests', () => { 5 | it('Works with strings', () => { 6 | expect(convert('abc')).toBe('abc\n\n'); 7 | }); 8 | 9 | it('Works with Buffer', () => { 10 | expect(convert(Buffer.from('abc', 'utf8'))).toBe('abc\n\n'); 11 | }); 12 | 13 | it('Converts strong and bold text', () => { 14 | const {source, target} = getTestStrings('bold'); 15 | 16 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 17 | }); 18 | 19 | it('Plaintext stays the same', () => { 20 | const {source, target} = getTestStrings('plaintext'); 21 | 22 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 23 | }); 24 | 25 | it('Simple table', () => { 26 | const {source, target} = getTestStrings('table'); 27 | 28 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 29 | }); 30 | 31 | it('Two new lines between paragraphs', () => { 32 | const {source, target} = getTestStrings('paragraph'); 33 | 34 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 35 | }); 36 | 37 | it('Converts HTML tags to "tag." style', () => { 38 | const {source, target} = getTestStrings('html'); 39 | 40 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 41 | }); 42 | 43 | it('Converts italics', () => { 44 | const {source, target} = getTestStrings('italics'); 45 | 46 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 47 | }); 48 | 49 | it('Converts strikethrough', () => { 50 | const {source, target} = getTestStrings('strikethrough'); 51 | 52 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 53 | }); 54 | 55 | it('Different types of headings', () => { 56 | const {source, target} = getTestStrings('headings'); 57 | 58 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 59 | }); 60 | 61 | it('Horizontal rule', () => { 62 | const {source, target} = getTestStrings('hr'); 63 | 64 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 65 | }); 66 | 67 | it('Converts br to newline', () => { 68 | const {source, target} = getTestStrings('br'); 69 | 70 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /tests/code.test.js: -------------------------------------------------------------------------------- 1 | const convert = require('..'); 2 | const getTestStrings = require('./utils/testStringLoader'); 3 | 4 | describe('Code tests', () => { 5 | it('Format with code fences', () => { 6 | const {source, target} = getTestStrings('code-fence'); 7 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 8 | }); 9 | 10 | it('Format with code indentation', () => { 11 | const {source, target} = getTestStrings('code-indentation'); 12 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 13 | }); 14 | 15 | it('Codespan brackets', () => { 16 | const {source, target} = getTestStrings('codespan-brackets'); 17 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 18 | }); 19 | 20 | it('Code HTML', () => { 21 | // The markdown processing treats this as NOT HTML, so it is 22 | // going to escape the & into & first. 23 | const {source, target} = getTestStrings('code-html'); 24 | expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 25 | }); 26 | // More code tests 27 | it('Add new language to language map', () => { 28 | const {source, target} = getTestStrings('code-languagemap'); 29 | expect( 30 | convert(source, { 31 | codeBlock: { 32 | languageMap: { 33 | leet: '1337', 34 | }, 35 | }, 36 | }), 37 | ).toStrictEqual(expect.stringContaining(target)); 38 | }); 39 | 40 | it("Don't collapse code", () => { 41 | const {source, target} = getTestStrings('code-not-collapsing'); 42 | expect( 43 | convert(source, { 44 | codeBlock: { 45 | options: { 46 | collapse: false, 47 | }, 48 | }, 49 | }), 50 | ).toStrictEqual(expect.stringContaining(target)); 51 | }); 52 | 53 | // it('Collapses if longer than 20 lines', () => { 54 | // const {source, target} = getTestStrings('code-collapsing-more-than-20'); 55 | // expect(convert(source)).toStrictEqual(expect.stringContaining(target)); 56 | // // {code:language=none|collapse=true}\n1\n2\n3\n{code} 57 | // }); 58 | 59 | // it('Collapses at a set number of lines', () => { 60 | // const {source, target} = getTestStrings( 61 | // 'code-collapsing-custom-number-lines', 62 | // ); 63 | // expect(convert(source), { 64 | // codeBlock: { 65 | // collapseAfter: 2, 66 | // }, 67 | // }).toStrictEqual(expect.stringContaining(target)); 68 | // // {code:language=none|collapse=true}\n1\n2\n3\n{code} 69 | // }); 70 | 71 | // // Codespan tests 72 | // it('changes unsafe text so Confluence understands it', () => { 73 | // expect(convert('`~/file` and `~/folder` and `{braces}`')).toBe( 74 | // '{{~/file}} and {{~/folder}} and {{{braces}}}', 75 | // ); 76 | // }); 77 | // it('preserves entities that are already HTML encoded', () => { 78 | // expect(convert('`Fish&Chips`')).toBe('{{Fish&Chips}}\n\n'); 79 | // expect(convert('`> and <`')).toBe('{{> and <}}\n\n'); 80 | // }); 81 | }); 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Markdown2Confluence 2 | 3 | This tool converts [Markdown] to [Confluence Wiki Markup]. 4 | 5 | [![NPM version][npm-image]][npm-url] 6 | [![Downloads][downloads-image]][downloads-url] 7 | 8 | ## Overview 9 | 10 | Using [Markdown] is fast becoming a standard for open-source projects and their documentation. There are a few variants, such as [GitHub Flavored Markdown], which add additional features. 11 | 12 | Atlassian's Confluence has a different way of writing documentation, according to their [Confluence Wiki Markup] and [later pages][confluence-wiki-markup] and [references][wiki-render-help-action]. 13 | 14 | This project contains a library and a command-line tool that bridges the gap and converts from Markdown to Confluence. 15 | 16 | ## Installation 17 | 18 | ```sh 19 | npm i -g @shogobg/markdown2confluence 20 | ``` 21 | 22 | ```sh 23 | npm i --save @shogobg/markdown2confluence 24 | ``` 25 | 26 | ## Command-Line Use 27 | 28 | Read in a Markdown file and write Confluence format to another file: 29 | 30 | ```sh 31 | markdown2confluence 32 | ``` 33 | 34 | Or output to standard output: 35 | 36 | markdown2confluence README.md 37 | 38 | Or pipe in a file and output to standard output: 39 | 40 | cat README.md | markdown2confluence 41 | 42 | (Piping into markdown2confluence only works on platforms that support /dev/stdin, for example not Windows.) 43 | 44 | ## As library dependency 45 | 46 | Or just edit your application `package.json` and add the following code to your `dependencies` object: 47 | 48 | { 49 | ... 50 | "dependencies": { 51 | ... 52 | "@shogobg/markdown2confluence": "*" 53 | ... 54 | } 55 | ... 56 | } 57 | 58 | Now you write some JavaScript to load Markdown content and convert. 59 | 60 | ```javascript 61 | markdown2confluence = require('@shogobg/markdown2confluence'); 62 | markdown = fs.readFileSync('README.md'); 63 | confluence = markdown2confluence(markdown); 64 | console.log(confluence); 65 | ``` 66 | 67 | This uses the wonderful [marked](https://www.npmjs.com/package/marked) library to parse and reformat the Markdown text. 68 | 69 | ## Custom options 70 | 71 | Since this tool uses [marked](https://www.npmjs.com/package/marked), there is a pre-defined renderer which we pass to [marked](https://www.npmjs.com/package/marked). 72 | If you want to replace any of the predefined functions or the renderer as a whole, you can do so by passing an options object to the tool. 73 | 74 | ```javascript 75 | markdown2confluence = require('@shogobg/markdown2confluence'); 76 | markdown = fs.readFileSync('README.md'); 77 | confluence = markdown2confluence(markdown, { 78 | renderer: { 79 | link: href => { 80 | return `http://example.com/${href}`; 81 | }, 82 | }, 83 | }); 84 | console.log(confluence); 85 | ``` 86 | 87 | Additionally, the options objects takes custom arguments for the confluence code block options. 88 | 89 | ```javascript 90 | markdown2confluence = require('@shogobg/markdown2confluence'); 91 | markdown = fs.readFileSync('README.md'); 92 | confluence = markdown2confluence(markdown, { 93 | renderer: { 94 | link: href => { 95 | return `http://example.com/${href}`; 96 | }, 97 | }, 98 | codeBlock: { 99 | // Adds support for new language 100 | languageMap: { 101 | leet: '1337', 102 | }, 103 | // Shows the supported options and their default values 104 | options: { 105 | title: 'none', 106 | language: 'none', 107 | borderStyle: 'solid', 108 | theme: 'RDark', // dark is good 109 | linenumbers: true, 110 | collapse: true, 111 | }, 112 | }, 113 | }); 114 | console.log(confluence); 115 | ``` 116 | 117 | ## Supported Markdown 118 | 119 | The aim of this library is to convert as much Markdown to Confluence Wiki Markup. As such, most Markdown is supported but there are going to be rare cases that are not supported (such as code blocks within lists) or other scenarios that people find. 120 | 121 | If it is possible to convert the Markdown to Confluence Wiki Markup (without resorting to HTML), then this library should be able to do it. If you find anything wrong, it is likely a bug and should be reported. I would need a sample of Markdown, the incorrect translation and the correct way to represent that in Confluence. Please file an issue with this information in order to help replicate and fix the issue. 122 | 123 | A good demonstration chunk of markdown is available in [demo.md](./demo/demo.md). 124 | 125 | **What does not work?** 126 | 127 | - HTML. It is copied verbatim to the output text. 128 | - Did you find anything else? Please tell us about it by opening an issue. 129 | 130 | ## License 131 | 132 | [![License][license-image]][license-url] 133 | 134 | # About 135 | 136 | This tool was originally written by [chunpu](https://github.com/chunpu/markdown2confluence), but it was outdated with latest version from 2017. 137 | It didn't suit my needs to convert Markdown generated by [widdershins](https://github.com/Mermade/widdershins), so I decided to update it and publish the changes. 138 | Shamelessly copied improvements from [fdian](https://github.com/connected-world-services/markdown2confluence-cws). 139 | 140 | [markdown]: http://daringfireball.net/projects/markdown/syntax 141 | [github flavored markdown]: https://github.github.com/gfm/ 142 | [confluence wiki markup]: https://confluence.atlassian.com/display/CONF42/Confluence+Wiki+Markup 143 | [npm-image]: https://img.shields.io/npm/v/@shogobg/markdown2confluence.svg?style=flat-square 144 | [npm-url]: https://www.npmjs.com/package/@shogobg/markdown2confluence 145 | [downloads-image]: http://img.shields.io/npm/dm/@shogobg/markdown2confluence.svg?style=flat-square 146 | [downloads-url]: https://www.npmjs.com/package/@shogobg/markdown2confluence 147 | [license-image]: http://img.shields.io/npm/l/@shogobg/markdown2confluence.svg?style=flat-square 148 | [license-url]: # 149 | [wiki-render-help-action]: https://roundcorner.atlassian.net/secure/WikiRendererHelpAction.jspa?section=all 150 | [confluence-wiki-markup]: https://confluence.atlassian.com/display/DOC/Confluence+Wiki+Markup 151 | [removed-wiki-markup-editor]: http://blogs.atlassian.com/2011/11/why-we-removed-wiki-markup-editor-in-confluence-4/ 152 | [code-block-macro]: https://confluence.atlassian.com/doc/code-block-macro-139390.html 153 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const {options, marked} = require('marked'); 2 | const qs = require('querystring'); 3 | 4 | const defaultLanguageMap = require('./defaultLanguageMap.json'); 5 | 6 | const codeBlockParams = { 7 | options: { 8 | title: 'none', 9 | language: 'none', 10 | borderStyle: 'solid', 11 | theme: 'RDark', // dark is good 12 | linenumbers: true, 13 | collapse: true, 14 | }, 15 | 16 | get(lang) { 17 | const codeOptions = this.options; 18 | codeOptions.language = lang; 19 | 20 | return codeOptions; 21 | }, 22 | }; 23 | 24 | const defaultRenderer = { 25 | /** 26 | * Simple text. 27 | * 28 | * @param {string} text 29 | * @return {string} 30 | */ 31 | text: function (text) { 32 | return text; 33 | }, 34 | /** 35 | * A paragraph of text. 36 | * 37 | * @param {string} text 38 | * @return {string} 39 | */ 40 | paragraph: function (text) { 41 | return `${text}\n\n`; 42 | }, 43 | /** 44 | * Embedded HTML. 45 | * 46 | *

My text

47 | * 48 | * turns into 49 | * 50 | * h1. My text 51 | * 52 | * @param {string} text 53 | * @return {string} 54 | */ 55 | html: function (text) { 56 | const regex = 57 | /<([\w]+)\s*[\w=]*"?([\/:\s\w=\-@\.\&\?\%]*)"?>([\/:\s\w.!?\\<>\-]*)(<\/\1>)?/gi; 58 | 59 | // We need special handling for anchors 60 | text = text.replace(regex, (match, tag, link, content) => { 61 | if (tag === 'a') { 62 | return `[${link}|`; 63 | } 64 | 65 | return `${tag}. ${content}`; 66 | }); 67 | 68 | // Closing anchor tag otherwise remove the closing tag 69 | text = text.replace(/<\/([\s\w]+)>/gi, (match, tag) => { 70 | if (tag === 'a') { 71 | return `]`; 72 | } 73 | 74 | return ''; 75 | }); 76 | 77 | return text; 78 | }, 79 | /** 80 | * Headings 1 through 6. 81 | * 82 | * Heading 1 83 | * ========= 84 | * 85 | * # Heading 1 alternate 86 | * 87 | * ###### Heading 6 88 | * 89 | * turns into 90 | * 91 | * h1. Heading 1 92 | * 93 | * h1. Heading 1 alternate 94 | * 95 | * h6. Heading 6 96 | * 97 | * @param {string} text 98 | * @param {number} level 99 | * @return {string} 100 | */ 101 | heading: function (text, level) { 102 | return `h${level}. ${text}\n\n`; 103 | }, 104 | /** 105 | * Creates strong text. 106 | * 107 | * This is typically **bolded**. 108 | * 109 | * becomes 110 | * 111 | * This is typically *bolded*. 112 | * 113 | * @param {string} text 114 | * @return {string} 115 | */ 116 | strong: function (text) { 117 | return `*${text}*`; 118 | }, 119 | /** 120 | * Emphasis. 121 | * 122 | * Typically this is *italicized* text. 123 | * 124 | * turns into 125 | * 126 | * Typically this is _italicized_ text. 127 | * 128 | * @param {string} text 129 | * @return {string} 130 | */ 131 | em: function (text) { 132 | return `_${text}_`; 133 | }, 134 | /** 135 | * Strikethrough. 136 | * 137 | * Supported ~~everywhere~~ in GFM only. 138 | * 139 | * turns into 140 | * 141 | * Supported -everywhere- in GFM only. 142 | * 143 | * @param {string} text 144 | * @return {string} 145 | */ 146 | del: function (text) { 147 | return `-${text}-`; 148 | }, 149 | /** 150 | * Inline code. 151 | * 152 | * Text that has statements, like `a = true` or similar. 153 | * 154 | * turns into 155 | * 156 | * Text that has statements, like {{a = true}} or similar. 157 | * 158 | * Be wary. This converts wrong: "Look at `~/file1` or `~/file2`" 159 | * Confluence thinks it is subscript and converts the markup into 160 | * "Look at /file1 or /file2". 161 | * That's why some characters need to be escaped. 162 | * 163 | * @param {string} text 164 | * @return {string} 165 | */ 166 | codespan: function (text) { 167 | text = text.split(/(&[^;]*;)/).map((match, index) => { 168 | // These are the delimeters. 169 | if (index % 2) { 170 | return match; 171 | } 172 | 173 | return match.replace(/[^a-zA-Z0-9 ]/g, (badchar) => { 174 | return `&#${badchar[0].charCodeAt(0)};`; 175 | }); 176 | }); 177 | 178 | return `{{${text.join('')}}}`; 179 | }, 180 | /** 181 | * Blockquote. 182 | * 183 | * > This is a blockquote. 184 | * 185 | * is changed into 186 | * 187 | * {quote} 188 | * This is a blockquote. 189 | * {quote} 190 | * 191 | * @param {string} text 192 | * @return {string} 193 | */ 194 | blockquote: function (text) { 195 | return `{quote}\n${text.trim()}\n{quote}\n\n`; 196 | }, 197 | /** 198 | * A line break. 199 | * This is triggered by having a backslash at the end of a row 200 | * Some text\ 201 | * 202 | * @return {string} 203 | */ 204 | br: function () { 205 | return '\n'; 206 | }, 207 | /** 208 | * Horizontal rule. 209 | * 210 | * --- 211 | * 212 | * turns into 213 | * 214 | * ---- 215 | * 216 | * @return {string} 217 | */ 218 | hr: function () { 219 | return '----\n\n'; 220 | }, 221 | /** 222 | * Link to another resource. 223 | * 224 | * [Home](/) 225 | * [Home](/ "some title") 226 | * 227 | * turns into 228 | * 229 | * [Home|/] 230 | * [some title|/] 231 | * 232 | * @param {string} href 233 | * @param {string} title 234 | * @param {string} text 235 | * @return {string} 236 | */ 237 | link: function (href, title, text) { 238 | // Sadly, one must choose if the link's title should be displayed 239 | // or the linked text should be displayed. We picked the linked text. 240 | text = text || title; 241 | 242 | if (text) { 243 | text += '|'; 244 | } 245 | 246 | return `[${text}${href}]`; 247 | }, 248 | /** 249 | * Converts a list. 250 | * 251 | * # ordered 252 | * * unordered 253 | * 254 | * becomes 255 | * 256 | * # ordered 257 | * #* unordered 258 | * 259 | * Note: This adds an extra "\r" before the list in order to cope 260 | * with nested lists better. When there's a "\r" in a nested list, it 261 | * is translated into a "\n". When the "\r" is left in the converted 262 | * result then it is removed. 263 | * 264 | * @param {string} text 265 | * @param {boolean} ordered 266 | * @return {string} 267 | */ 268 | list: function (text, ordered) { 269 | text = text.trim(); 270 | 271 | if (ordered) { 272 | text = text.replace(/^\*/gm, '#'); 273 | } 274 | 275 | return `\r${text}\n\n`; 276 | }, 277 | /** 278 | * Changes a list item. Always marks it as an unordered list, but 279 | * list() will change it back. 280 | * 281 | * @param {string} text 282 | * @return {string} 283 | */ 284 | listitem: function (text) { 285 | // If a list item has a nested list, it will have a "\r" in the 286 | // text. Turn that "\r" into "\n" but trim out other whitespace 287 | // from the list. 288 | text = text.replace(/\s*$/, '').replace(/\r/g, '\n'); 289 | 290 | // Convert newlines followed by a # or a * into sub-list items 291 | text = text.replace(/\n([*#])/g, '\n*$1'); 292 | 293 | return `* ${text}\n`; 294 | }, 295 | /** 296 | * An embedded image. 297 | * 298 | * ![alt-text](image-url) 299 | * 300 | * is changed into 301 | * 302 | * !image-url! 303 | * 304 | * Markdown supports alt text and titles. Confluence does not. 305 | * 306 | * @param {string} href 307 | * @return {string} 308 | */ 309 | image: function (href) { 310 | return `!${href}!`; 311 | }, 312 | /** 313 | * Renders a table. Most of the work is done in tablecell. 314 | * 315 | * @param {string} header 316 | * @param {string} body 317 | * @return {string} 318 | */ 319 | table: function (header, body) { 320 | return `${header}${body}\n`; 321 | }, 322 | /** 323 | * Converts a table row. Most of the work is done in tablecell, however 324 | * that can't tell if the cell is at the end of a row or not. Get the 325 | * first cell's leading boundary and remove the double-boundary marks. 326 | * 327 | * @param {string} text 328 | * @return {string} 329 | */ 330 | tablerow: function (text) { 331 | var boundary; 332 | 333 | boundary = text.match(/^\|*/); 334 | 335 | if (boundary) { 336 | boundary = boundary[0]; 337 | } else { 338 | boundary = '|'; 339 | } 340 | 341 | return `${text}${boundary}\n`; 342 | }, 343 | /** 344 | * Converts a table cell. When this is a header, the cell is prefixed 345 | * with two bars instead of one. 346 | * 347 | * @param {string} text 348 | * @param {Object} flags 349 | * @return {string} 350 | */ 351 | tablecell: function (text, flags) { 352 | var boundary; 353 | 354 | if (flags.header) { 355 | boundary = '||'; 356 | } else { 357 | boundary = '|'; 358 | } 359 | 360 | return `${boundary}${text}`; 361 | }, 362 | /** 363 | * Code block. 364 | * 365 | * ```js 366 | * // JavaScript code 367 | * ``` 368 | * 369 | * is changed into 370 | * 371 | * {code:language=javascript|borderStyle=solid|theme=RDark|linenumbers=true|collapse=true} 372 | * // JavaScript code 373 | * {code} 374 | * 375 | * @param {string} text 376 | * @param {string} lang 377 | * @return {string} 378 | */ 379 | code: function (text, lang) { 380 | lang = defaultLanguageMap[(lang ?? '').toLowerCase()]; 381 | 382 | const param = qs.stringify(codeBlockParams.get(lang), '|', '='); 383 | return `{code:${param}}\n${text}\n{code}\n\n`; 384 | }, 385 | }; 386 | 387 | const markdown2confluence = (markdown, options) => { 388 | if (options) { 389 | const {codeBlock, renderer} = options; 390 | 391 | if (codeBlock && codeBlock.languageMap) { 392 | Object.entries(codeBlock.languageMap).forEach((option) => { 393 | defaultLanguageMap[option[0]] = option[1]; 394 | }); 395 | } 396 | 397 | if (codeBlock && codeBlock.options) { 398 | Object.entries(codeBlock.options).forEach((option) => { 399 | if ( 400 | codeBlockParams.options[option[0]] && 401 | typeof option[1] !== 'function' 402 | ) { 403 | codeBlockParams.options[option[0]] = option[1]; 404 | } 405 | }); 406 | } 407 | 408 | if (renderer) { 409 | Object.entries(renderer).forEach((option) => { 410 | if (defaultRenderer[option[0]] && typeof option[1] === 'function') { 411 | defaultRenderer[option[0]] = option[1]; 412 | } 413 | }); 414 | } 415 | } 416 | 417 | marked.use({renderer: defaultRenderer}); 418 | 419 | return marked.parse(markdown.toString()); 420 | }; 421 | 422 | module.exports = markdown2confluence; 423 | --------------------------------------------------------------------------------