├── 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 | 
--------------------------------------------------------------------------------
/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&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 | 
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('', {
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 | * 
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 |
--------------------------------------------------------------------------------