├── .github └── workflows │ ├── browserstack.yml │ ├── codeql-analysis.yml │ ├── docs.yml │ ├── node.linux.yml │ └── node.win.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── CREDITS.md ├── DONATIONS.md ├── LICENSE ├── README.md ├── SECURITY.md ├── TASKS.TODO.md ├── bin └── showdown.js ├── bower.json ├── dist ├── showdown.js ├── showdown.js.map ├── showdown.min.js └── showdown.min.js.map ├── docs ├── assets │ ├── extra.css │ └── markdown-editor.png ├── available-options.md ├── cli.md ├── compatibility.md ├── configuration.md ├── create-extension.md ├── credits.md ├── donations.md ├── event_system.md ├── extensions-list.md ├── extensions.md ├── flavors.md ├── index.md ├── integrations.md ├── markdown-syntax.md ├── quickstart.md ├── tutorials │ ├── add-default-class-to-html.md │ ├── index.md │ ├── markdown-editor-with-showdown.md │ └── use-both-extension-types-together.md └── xss.md ├── karma.browserstack.js ├── karma.conf.js ├── mkdocs.yml ├── package-lock.json ├── package.json └── src ├── cli └── cli.js ├── converter.js ├── helpers.js ├── loader.js ├── options.js ├── showdown.js └── subParsers ├── makehtml ├── blockGamut.js ├── blockQuotes.js ├── codeBlocks.js ├── codeSpans.js ├── completeHTMLDocument.js ├── detab.js ├── ellipsis.js ├── emoji.js ├── encodeAmpsAndAngles.js ├── encodeBackslashEscapes.js ├── encodeCode.js ├── escapeSpecialCharsWithinTagAttributes.js ├── githubCodeBlocks.js ├── hashBlock.js ├── hashCodeTags.js ├── hashElement.js ├── hashHTMLBlocks.js ├── hashHTMLSpans.js ├── hashPreCodeTags.js ├── headers.js ├── horizontalRule.js ├── images.js ├── italicsAndBold.js ├── links.js ├── lists.js ├── metadata.js ├── outdent.js ├── paragraphs.js ├── runExtension.js ├── spanGamut.js ├── strikethrough.js ├── stripLinkDefinitions.js ├── tables.js ├── underline.js └── unescapeSpecialChars.js └── makemarkdown ├── blockquote.js ├── break.js ├── codeBlock.js ├── codeSpan.js ├── emphasis.js ├── header.js ├── hr.js ├── image.js ├── input.js ├── links.js ├── list.js ├── listItem.js ├── node.js ├── paragraph.js ├── pre.js ├── strikethrough.js ├── strong.js ├── table.js ├── tableCell.js └── txt.js /.github/workflows/browserstack.yml: -------------------------------------------------------------------------------- 1 | name: 'BrowserStack Test' 2 | 3 | on: 4 | push: 5 | branches: [ master, develop ] 6 | pull_request: 7 | branches: [ master, develop ] 8 | 9 | jobs: 10 | ubuntu-job: 11 | name: 'BrowserStack Test on Ubuntu' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: set up env vars 15 | # Only the first line of commit msg 16 | run: echo "COMMIT_MSG=$(printf "%s" "${{ github.event.head_commit.message }}" | head -n 1)" >> $GITHUB_ENV 17 | 18 | - name: '📦 Checkout the repository' 19 | uses: actions/checkout@v2 20 | 21 | - name: '🚚 Upgrade NPM' 22 | run: npm install -g npm 23 | 24 | - name: '⚙ Setup Node.js v17.x' 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: 17.x 28 | cache: 'npm' 29 | 30 | - name: '📖 Get current package version' 31 | id: package-version 32 | uses: martinbeentjes/npm-get-version-action@v1.2.3 33 | 34 | - name: '📝 Print build version and commit msg' 35 | run: 'printf "version: %s\n build:%s\n message:%s\n" "${{ steps.package-version.outputs.current-version}}" "${{ github.run_id }}" "$COMMIT_MSG"' 36 | 37 | - name: '📱 BrowserStack Env Setup' # Invokes the setup-env action 38 | uses: browserstack/github-actions/setup-env@master 39 | with: 40 | username: ${{ secrets.BROWSERSTACK_USERNAME }} 41 | access-key: ${{ secrets.BROWSERSTACK_ACCESSKEY }} 42 | project-name: 'showdown' 43 | build-name: ${{ steps.package-version.outputs.current-version}}-${{ github.run_id }} 44 | 45 | - name: '🚇 BrowserStack Local Tunnel Setup' # Invokes the setup-local action 46 | uses: browserstack/github-actions/setup-local@master 47 | with: 48 | local-testing: start 49 | local-identifier: random 50 | 51 | - name: '🚚 Install dependencies for CI' 52 | run: npm ci 53 | 54 | - name: '🏗 Building src files for testing' 55 | run: npx grunt concat:test 56 | 57 | - name: '✅ Running test on BrowserStack with Karma' 58 | run: npx karma start karma.browserstack.js 59 | 60 | - name: '🛑 BrowserStackLocal Stop' # Terminating the BrowserStackLocal tunnel connection 61 | uses: browserstack/github-actions/setup-local@master 62 | with: 63 | local-testing: stop 64 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master, develop ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master, develop ] 20 | schedule: 21 | - cron: '39 3 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'mkdocs.yml' 9 | - 'docs/**' 10 | - '.github/workflows/docs.yml' 11 | 12 | jobs: 13 | build_docs: 14 | name: Build documentation 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Trigger external build 20 | env: 21 | TOKEN: ${{ secrets.DOCS_DEPLOY_KEY }} 22 | COMMIT: ${{ github.event.head_commit.id }} 23 | COMMITTER: ${{ github.event.head_commit.author.username }} 24 | OWNER: showdownjs 25 | REPO: showdownjs.github.io 26 | run: | 27 | curl -X POST \ 28 | -H "Authorization: token ${TOKEN}" \ 29 | -H "Accept: application/vnd.github.v3+json" \ 30 | https://api.github.com/repos/${OWNER}/${REPO}/dispatches \ 31 | -d '{ "event_type": "e: \"'"${COMMIT}"'\" by '"${COMMITTER}"'", "client_payload": { "source": "showdown" } }' 32 | -------------------------------------------------------------------------------- /.github/workflows/node.linux.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node Linux CI 5 | 6 | on: 7 | push: 8 | branches: [ master, develop ] 9 | pull_request: 10 | branches: [ master, develop ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x, 14.x, 16.x, 17.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | 25 | - name: 🚚 Upgrade NPM 26 | run: npm install -g npm 27 | 28 | - name: Use Node.js ${{ matrix.node-version }} 29 | uses: actions/setup-node@v2 30 | with: 31 | node-version: ${{ matrix.node-version }} 32 | cache: 'npm' 33 | - run: npm ci 34 | - run: npm test 35 | -------------------------------------------------------------------------------- /.github/workflows/node.win.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node Windows CI 5 | 6 | on: 7 | push: 8 | branches: [ master, develop ] 9 | pull_request: 10 | branches: [ master, develop ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: windows-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x, 14.x, 16.x, 17.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm test 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | If you wish to contribute please read the following quick guide. 5 | 6 | # Issues (bug reporting) 7 | Generally, bug reports should be in the following format: 8 | 9 | 1. Description (Brief description of the problem) 10 | 2. Input (input that is causing problems) 11 | 3. Expected Output (Output that is expected) 12 | 4. Actual Output (Actual showdown output) 13 | 14 | ex: 15 | 16 | **Description:** 17 | Double asterisks do not parse as bold 18 | 19 | **Input:** 20 | 21 | hello **world**! 22 | 23 | **Expected output:** 24 | 25 |

hello world! 26 | 27 | **Actual Output:** 28 | 29 |

hello **world**!

30 | 31 | 32 | 33 | # Want a Feature? 34 | You can request a new feature by submitting an issue. If you would like to implement a new feature feel free to issue a 35 | Pull Request. 36 | 37 | 38 | # Pull requests (PRs) 39 | PRs are awesome. However, before you submit your pull request consider the following guidelines: 40 | 41 | - Search GitHub for an open or closed Pull Request that relates to your submission. You don't want to duplicate effort. 42 | - When issuing PRs that change code, make your changes in a new git branch based on develop: 43 | 44 | ```bash 45 | git checkout -b my-fix-branch develop 46 | ``` 47 | 48 | - Run the full test suite before submitting and make sure all tests pass (obviously =P). 49 | - Try to follow our [**coding style rules**](https://github.com/showdownjs/code-style/blob/master/README.md). 50 | Breaking them prevents the PR to pass the tests. 51 | - Refrain from fixing multiple issues in the same pull request. It's preferable to open multiple small PRs instead of one 52 | hard to review big one. Also, don't reuse old forks (or PRs) to fix new issues. 53 | - If the PR introduces a new feature or fixes an issue, please add the appropriate test case. 54 | - We use commit notes to generate the changelog. It's extremely helpful if your commit messages adhere to the 55 | [**AngularJS Git Commit Guidelines**](https://github.com/showdownjs/code-style/blob/master/README.md#commit-message-convention). 56 | - If we suggest changes then: 57 | - Make the required updates. 58 | - Re-run the Angular test suite to ensure tests are still passing. 59 | - Rebase your branch and force push to your GitHub repository (this will update your Pull Request): 60 | 61 | ```bash 62 | git rebase develop -i 63 | git push origin my-fix-branch -f 64 | ``` 65 | - After your pull request is merged, you can safely delete your branch. 66 | 67 | If you have time to contribute to this project, we feel obliged that you get credit for it. 68 | These rules enable us to review your PR faster and will give you appropriate credit in your GitHub profile. 69 | We thank you in advance for your contribution! 70 | 71 | 72 | # Joining the team 73 | We're looking for members to help maintaining Showdown. 74 | Please see [this issue](https://github.com/showdownjs/showdown/issues/114) to express interest or comment on this note. 75 | -------------------------------------------------------------------------------- /CREDITS.md: -------------------------------------------------------------------------------- 1 | Credits 2 | ======= 3 | 4 | - Showdown v2 5 | * [Estevão Santos](https://github.com/tivie) 6 | * [SyntaxRules](https://github.com/SyntaxRules) 7 | 8 | - Showdown v1 9 | * [Estevão Santos](https://github.com/tivie) 10 | * [Pascal Deschênes](https://github.com/pdeschen) 11 | 12 | - Showdown v0 13 | * [Corey Innis](http://github.com/coreyti):
14 | Original GitHub project maintainer 15 | * [Remy Sharp](https://github.com/remy/):
16 | CommonJS-compatibility and more 17 | * [Konstantin Käfer](https://github.com/kkaefer/):
18 | CommonJS packaging 19 | * [Roger Braun](https://github.com/rogerbraun):
20 | Github-style code blocks 21 | * [Dominic Tarr](https://github.com/dominictarr):
22 | Documentation 23 | * [Cat Chen](https://github.com/CatChen):
24 | Export fix 25 | * [Titus Stone](https://github.com/tstone):
26 | Mocha tests, extension mechanism, and bug fixes 27 | * [Rob Sutherland](https://github.com/roberocity):
28 | The idea that lead to extensions 29 | * [Pavel Lang](https://github.com/langpavel):
30 | Code cleanup 31 | * [Ben Combee](https://github.com/unwiredben):
32 | Regex optimization 33 | * [Adam Backstrom](https://github.com/abackstrom):
34 | WebKit bugfix 35 | * [Pascal Deschênes](https://github.com/pdeschen):
36 | Grunt support, extension fixes + additions, packaging improvements, documentation 37 | * [Estevão Santos](https://github.com/tivie)
38 | Bug fixing and late maintainer 39 | * [Hannah Wolfe](https://github.com/ErisDS)
40 | Bug fixes 41 | * [Alexandre Courtiol](https://github.com/acourtiol)
42 | Bug fixes and build optimization 43 | * [Karthik Balakrishnan](https://github.com/torcellite)
44 | Support for table alignment 45 | * [rheber](https://github.com/rheber)
46 | Cli 47 | 48 | 49 | - Original Project 50 | * [John Gruber](http://daringfireball.net/projects/markdown/)
51 | Author of Markdown 52 | * [John Fraser](http://attacklab.net/)
53 | Author of Showdown 54 | -------------------------------------------------------------------------------- /DONATIONS.md: -------------------------------------------------------------------------------- 1 | We would like to thank everyone that contributed to this library. If you find our library useful and wish to donate as well, you can do so [through patreon](https://www.patreon.com/showdownjs) or directly [through paypal](https://www.paypal.me/tiviesantos)!! Your contribution will be greatly appreciated. 2 | 3 | # Sponsors 4 | 5 | ## Platinum Supporters 6 | 7 | ## Gold Supporters 8 | 9 | ## Silver Supporters 10 | 11 | 12 | # Patron Supporters 13 | 14 | ## Awesome Supporter 15 | 16 | ## Cool Supporter 17 | 18 | ## Supporter 19 | 20 | - Ricardo Jordão 21 | 22 | - Tiago Silva 23 | 24 | 25 | --- 26 | 27 | # One Time Donors 28 | 29 | - [**Learn on demand Systems**](http://www.learnondemandsystems.com/) (1000$) 30 | 31 | - **Nothing AG** (25€) 32 | 33 | > Thank you for developing Showdown :) 34 | 35 | - **Sam Huffman** (15$) 36 | 37 | > Thanks for the great work on showdown.js! I've been looking for a good solution to serve wiki-like text to a browser and render as 38 | HTML but nearly gave up after mixed success with wikitext. Your library gets me very close to where I want to be. 39 | 40 | - **Maya Lekova** (10$) 41 | 42 | > Great work with the showdown library! I just used it and it worked exactly the way I expected 43 | (given a complex inline HTML inside the Markdown, which renders fine with other viewers). 44 | The other libraries I've tried (markdown-it and marked) produced a lot of bogus output. Y 45 | our library saved me at least half a day, thanks! Good luck :) 46 | 47 | - **Lin Wang** (10$) 48 | 49 | - **Walter Schnell** (10$) 50 | 51 | - **ivanhjc** (5$) 52 | 53 | - **Asbjørn Ulsberg** (5$) 54 | 55 | > Thanks for the ShowdownJS support! 56 | 57 | - **Juan Marcelo Russo** (1$) 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018,2021 ShowdownJS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Security fixes are addressed for the following versions of Showdown. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 2.0.x | :white_check_mark: | 10 | | 1.x.x | :x: (Known security issue with yargs dependecy) | 11 | 12 | Showdown targets the node.js versions targeted in the [node.js release schedule](https://nodejs.org/en/about/releases/). Our test suite follows this release schedule. Consequently, older versions of node may become unusable. 13 | 14 | ## Reporting a Vulnerability 15 | 16 | To report a vulnerability, please add an issue to our main github page: https://github.com/showdownjs/showdown/issues 17 | -------------------------------------------------------------------------------- /TASKS.TODO.md: -------------------------------------------------------------------------------- 1 | # ROADMAP TO VERSION 3.0 2 | 3 | 4 | ## Options 5 | 6 | - [ ] **ghCompatibleHeaderId** (removal) 7 | 8 | Will be removed and **will become the default behavior**. 9 | 10 | - [ ] **customizedHeaderId** (removal) 11 | 12 | This option introduced non compliant syntax so it really belongs in an extension. 13 | The new **listener extension system** allows users to directly modify and customize 14 | the HTML and add any attributes they wish. 15 | 16 | - [ ] **rawPrefixHeaderId** (removal) 17 | 18 | This option will be superseeded by the option `rawHeaderId`. So basically activating `rawHeaderId` will make 19 | showdown only to replace spaces, ', ", > and < with dashes (-) from generated header ids, including prefixes. 20 | 21 | - [X] **literalMidWordAsterisks** (removal) 22 | 23 | This option is weird, hard to maintain and really... makes little sense. 24 | 25 | - [X] **excludeTrailingPunctuationFromURLs** (removal) 26 | 27 | This option will be removed and will be the default behavior from now on. 28 | 29 | - [ ] **strikethrough** (change) 30 | 31 | Will be enabled by default 32 | 33 | - [ ] **disableForced4SpacesIndentedSublists** (to think/postpone) 34 | 35 | This was only a temporary option for backwards compatibility reason. However, most flavours support lists indented 36 | with 2 spaces, so it puts us in a tight spot, specially since some markdown beautifiers out there insist in 37 | indenting lists with 2 spaces, probably in a misguided try to follow the CommonMark spec. 38 | 39 | The CommonMark spec is, IMHO, a bit confusing for users regarding this, since list sub-blocks (and lists) 40 | are determined by the spaces from the start of the line and the first character after the list mark. And the proof 41 | are the MD beautifiers out there, which misinterpreted the spec and made a mess 42 | 43 | Showdown syntax is actually easier (and fully compliant with the original spec): if you indent something 4 spaces, 44 | it becomes a sub-block. Since lists are blocks, you must indent it 4 spaces for it to become a sub-block. 45 | 46 | Regardless, we kinda have 2 solutions: 47 | 48 | - Drop support for 2 space indentation (and make a lot of users confused since GFM, CommonMark and others allow this) 49 | - Create a new list subparser that can be turned on with an option, like gfmListStyle 50 | (but postpones even more the alpha 3.0 release since the list subparser is probably the hardest thing to rewrite) 51 | 52 | Tough choices... 53 | 54 | - [ ] **simpleLineBreaks** (change) 55 | 56 | Will be removed from Github Flavor since github only does this in comments (which is weird...) 57 | 58 | - [ ] **openLinksInNewWindow** (removal) 59 | 60 | Will be removed in favor of the new listener extension, which will allow users to manipulate HTML tags attributes 61 | directly. 62 | 63 | - [ ] Revamp the option system 64 | 65 | Revamp the option system so that it becomes more simple. Right now, it's really confusing. And option names are weird 66 | too. The idea is to pass options to the constructor under an option object, that can have hierarchical structure. 67 | Ex: 68 | 69 | ```js 70 | var conv = new showdown.Converter({ 71 | options: { 72 | links: { 73 | autoLinks: true 74 | }, 75 | headings: { 76 | startLevel: 2 77 | } 78 | } 79 | }); 80 | ``` 81 | 82 | ## Legacy Code Removal 83 | - [ ] Legacy extension support 84 | 85 | Old extensions that inject directly into extensions object property will no longer be supported 86 | 87 | - [ ] HTML and OUTPUT extensions 88 | 89 | HTML and OTP extensions will be dropped in favor of Listener Extensions. We might even give them a new name 90 | 91 | ## Subparsers 92 | - [X] **Anchors**: Revamp the anchors subparser so it calls strikethrough, bold, italic and underline directly 93 | - [X] **autoLinks**: Fix some lingering bugs and issues with autolinks 94 | 95 | ## Priority Bugs 96 | - [X] **#355**: *simplifiedAutoLink URLs inside parenthesis followed by another character are not parsed correctly* 97 | - [X] **#534**: *Multiple parentheses () in url link is not parsed correctly* 98 | - [ ] **#367**: *sublists rendering with 2 spaces* - related to disableForced4SpacesIndentedSublists option... 99 | - [ ] **#537**: *master branch doesn't work in a web worker* 100 | 101 | ## CLI 102 | - [ ] Refactor the CLI 103 | - [ ] **#381**: *Support for src and dst directories in showdown cli* 104 | - [X] **#584**: *Fails to read from stdin* 105 | - [X] **#554**: *CLI not working with jsdom v10* 106 | 107 | ## Other stuff 108 | - [X] Regexp rewrite for more performance oompf 109 | - [X] Full unit testing 110 | - [ ] Better error reporting 111 | 112 | ## Stuff that probably won't make it to v2.0 113 | - [ ] **#486**: *A backslash at the end of the line is a hard line break* 114 | - [ ] **#548**: *anchors and images of subParser are errors when they are specific strings* 115 | - [ ] **#549**: *Strange parsing issue with `
`*
116 | - [ ] Rethink the global variable
117 | 
118 | ## NEW Features
119 | 
120 | ### Event system
121 | - [X] Listener system revamp
122 | - [ ] Standardize events for all event types
123 | - [ ] Unit testing
124 | - [ ] Functional testing
125 | 
126 | This should address:
127 | - **#567**: Allow not making header ids lowercase
128 | - **#540**: Add complete class to the tasklist list element
129 | 
130 | ### MD to HTML conversion
131 | - [X] Basic support
132 | - [X] Basic functional testcase
133 | - [ ] Advanced support for all showdown MD features
134 | - [ ] Advanced functional testcase
135 | - [ ] Unit testing
136 | 
137 | ## Documentation (for v2.0)
138 | - [ ] Options
139 | - [ ] Extensions (and the new event system)
140 | - [ ] Cookbook (with stuff for backwards compatibility, specially regarding removed options)
141 | 
142 | ## Browser Testing
143 | 
144 | - [X] Implement unit tests in Karma
145 | - [ ] Implement functional tests in Karma
146 | - [ ] Integrate with browserstack
147 | 


--------------------------------------------------------------------------------
/bin/showdown.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | var showdown,version,fs=require("fs"),path=require("path"),Command=require("commander").Command,program=new Command,path1=path.resolve(__dirname+"/../dist/showdown.js"),path2=path.resolve(__dirname+"/../../.build/showdown.js");function Messenger(o,t,e){"use strict";t=!!t||!!e,e=!!e,this._print="stdout"===(o=o||"stderr")?console.log:console.error,this.errorExit=function(o){e||(console.error("ERROR: "+o.message),console.error("Run 'showdown  -h' for help")),process.exit(1)},this.okExit=function(){e||(this._print("\n"),this._print("DONE!")),process.exit(0)},this.printMsg=function(o){t||e||!o||this._print(o)},this.printError=function(o){e||console.error(o)}}function showShowdownOptions(){"use strict";var o,t=showdown.getDefaultOptions(!1);for(o in console.log("\nshowdown makehtml config options:"),t)t.hasOwnProperty(o)&&console.log("  "+o+":","[default="+t[o].defaultValue+"]",t[o].describe);console.log('\n\nExample: showdown makehtml -c openLinksInNewWindow ghMentions ghMentionsLink="https://google.com"')}function parseShowdownOptions(o,t){"use strict";var e=t;if(o)for(var n=0;n [options]").option("-q, --quiet","Quiet mode. Only print errors").option("-m, --mute","Mute mode. Does not print anything"),program.command("makehtml").description("Converts markdown into html").addHelpText("after","\n\nExamples:").addHelpText("after","  showdown makehtml -i                     Reads from stdin and outputs to stdout").addHelpText("after","  showdown makehtml -i foo.md -o bar.html  Reads 'foo.md' and writes to 'bar.html'").addHelpText("after",'  showdown makehtml -i --flavor="github"   Parses stdin using GFM style').addHelpText("after","\nNote for windows users:").addHelpText("after","When reading from stdin, use option -u to set the proper encoding or run `chcp 65001` prior to calling showdown cli to set the command line to utf-8").option("-i, --input [file]","Input source. Usually a md file. If omitted or empty, reads from stdin. Windows users see note below.",!0).option("-o, --output [file]","Output target. Usually a html file. If omitted or empty, writes to stdout",!0).option("-u, --encoding ","Sets the input encoding","utf8").option("-y, --output-encoding ","Sets the output encoding","utf8").option("-a, --append","Append data to output instead of overwriting. Ignored if writing to stdout",!1).option("-e, --extensions ","Load the specified extensions. Should be valid paths to node compatible extensions").option("-p, --flavor ","Run with a predetermined flavor of options. Default is vanilla","vanilla").option("-c, --config ","Enables showdown makehtml parser config options. Overrides flavor").option("--config-help","Shows configuration options for showdown parser").action(makehtmlCommand),program.parse();


--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
 1 | {
 2 |     "name": "showdown",
 3 |     "description": "A Markdown to HTML converter written in Javascript",
 4 |     "homepage": "https://github.com/showdownjs/showdown",
 5 |     "authors": [
 6 |       "Estevão Santos (https://github.com/tivie)",
 7 |       "Pascal Deschênes (https://github.com/pdeschen)"
 8 |     ],
 9 |     "main": ["dist/showdown.js"],
10 |     "ignore": [
11 |       ".editorconfig",
12 |       ".gitattributes",
13 |       ".gitignore",
14 |       ".jscs.json",
15 |       ".jshintignore",
16 |       ".jshintrc",
17 |       ".travis.yml",
18 |       "Gruntfile.js",
19 |       "package.json",
20 |       "test/*"
21 |     ],
22 |     "repository": {
23 |       "type": "git",
24 |       "url": "https://github.com/showdownjs/showdown.git"
25 |     },
26 |     "keywords": [
27 |       "markdown",
28 |       "md",
29 |       "mdown"
30 |     ],
31 |     "license": "https://github.com/showdownjs/showdown/blob/master/license.txt"
32 | }
33 | 


--------------------------------------------------------------------------------
/docs/assets/extra.css:
--------------------------------------------------------------------------------
1 | :root {
2 |   --md-primary-fg-color: rgb(196, 54, 39);
3 |   --md-accent-fg-color: rgb(62, 139, 138);
4 | }


--------------------------------------------------------------------------------
/docs/assets/markdown-editor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/showdownjs/showdown/95255984ad80acf745ed74605bd3ad8357dc9b33/docs/assets/markdown-editor.png


--------------------------------------------------------------------------------
/docs/cli.md:
--------------------------------------------------------------------------------
  1 | Showdown comes bundled with a Command-line interface (CLI) tool that allows you to run Showdown converter from the command line.
  2 | 
  3 | ## Requirements
  4 | 
  5 | * [Node.js](https://nodejs.org/en/)
  6 | 
  7 | ## Quick start guide
  8 | 
  9 | 1. Check that Showdown CLI is accessible.
 10 | 
 11 |     * If you installed Showdown globally via `npm install showdown -g`, you can access the CLI tool help by typing `showdown -h` in the command line:
 12 | 
 13 |         === "input"
 14 | 
 15 |             ```sh
 16 |             showdown -h
 17 |             ```
 18 | 
 19 |         === "output"
 20 | 
 21 |             ```
 22 |             Usage: showdown  [options]
 23 | 
 24 |             CLI to Showdownjs markdown parser v3.0.0-alpha
 25 | 
 26 |             Options:
 27 |               -V, --version       output the version number
 28 |               -q, --quiet         Quiet mode. Only print errors
 29 |               -m, --mute          Mute mode. Does not print anything
 30 |               -h, --help          display help for command
 31 | 
 32 |             Commands:
 33 |               makehtml [options]  Converts markdown into html
 34 |               help [command]      display help for command
 35 |             ```
 36 | 
 37 |     * If you installed Showdown locally via `npm install showdown`, open the folder where Showdown is installed, and type `node ./bin/showdown.js -h` in the command line:
 38 | 
 39 |         === "input"
 40 | 
 41 |             ```sh
 42 |             node ./bin/showdown.js -h
 43 |             ```
 44 | 
 45 |         === "output"
 46 | 
 47 |             ```
 48 |             Usage: showdown  [options]
 49 | 
 50 |             CLI to Showdownjs markdown parser v3.0.0-alpha
 51 | 
 52 |             Options:
 53 |               -V, --version       output the version number
 54 |               -q, --quiet         Quiet mode. Only print errors
 55 |               -m, --mute          Mute mode. Does not print anything
 56 |               -h, --help          display help for command
 57 | 
 58 |             Commands:
 59 |               makehtml [options]  Converts markdown into html
 60 |               help [command]      display help for command
 61 |             ```
 62 | 
 63 | 1. Use `makehtml` command to convert your document to HTML. For example:
 64 | 
 65 |     !!! example "Convert `foo.md` into `bar.html`"
 66 |         
 67 |         ```sh
 68 |         showdown makehtml -i foo.md -o bar.html
 69 |         ```
 70 | 
 71 | ## Commands
 72 | 
 73 | ### `makehtml`
 74 | 
 75 | Convert a Markdown input into HTML.
 76 | 
 77 | **Usage**
 78 | 
 79 | ```sh
 80 | showdown makehtml [options]
 81 | ```
 82 | 
 83 | #### Options
 84 | 
 85 | ###### `-i / --input`
 86 | 
 87 | * Short format: `-i`
 88 | * Alias: `--input`
 89 | * Description: Input source. Usually a `.md` file. If omitted or empty, reads from `stdin`.
 90 | * Examples:
 91 | 
 92 |     !!! example ""
 93 | 
 94 |         ```sh
 95 |         // Read from stdin and output to stdout
 96 |         showdown makehtml -i
 97 | 
 98 |         // Read from the foo.md file and output to stdout
 99 |         showdown makehtml --input foo.md
100 |         ```
101 | 
102 | ###### `-o/--output`
103 | 
104 | * Short format: `-o`
105 | * Alias: `--output`
106 | * Description: Output target. Usually a `.html` file. If omitted or empty, writes to `stdout`.
107 | * Example:
108 | 
109 |     !!! example ""
110 | 
111 |         ```sh
112 |         // Read from the foo.md file and output to bar.html
113 |         showdown makehtml -i foo.md -o bar.html
114 |         ```
115 | 
116 | ###### `-a/--append`
117 | 
118 | * Short format: `-a`
119 | * Alias: `--append`
120 | * Description: Append data to output instead of overwriting.
121 | * Example: 
122 | 
123 |     !!! example ""
124 | 
125 |         ```sh
126 |         showdown makehtml -a
127 |         ```
128 | 
129 | ###### `-u/--encoding`
130 | 
131 | * Short format: `-u`
132 | * Alias: `--encoding`
133 | * Description: Specify the input encoding.
134 | * Example: 
135 |     
136 |     !!! example ""
137 | 
138 |         ```sh
139 |         showdown makehtml -u UTF8
140 |         ```
141 | 
142 | ###### `-e/--extensions`
143 | 
144 | * Short format: `-e`
145 | * Alias: `--extension`
146 | * Description: Load the specified extension(s). Should be valid path(s) to Node-compatible extensions.
147 | * Example:
148 | 
149 |     !!! example ""
150 | 
151 |         ```sh
152 |         showdown makehtml -e ~/twitter.js -e ~/youtube.js
153 |         ```
154 | 
155 | ###### `-c/--config`
156 | 
157 | * Short format: `-c`
158 | * Alias: `--config`
159 | * Description: Enable or disable parser options.
160 | * Introduced in: `2.0.1` (Breaking change. See the [`Extra options`](#extra-options) section below)
161 | * Example: 
162 | 
163 |     !!! example ""
164 | 
165 |         ```sh
166 |         showdown makehtml -i foo.md -o bar.html -c strikethrough
167 |         showdown makehtml -i foo.md -o bar.html -c strikethrough -c emoji
168 |         ```
169 | 
170 | ## Extra options
171 | 
172 | Starting from the version `2.0.1`, CLI the format of passing extra options has changed. Please make the necessary changes to your code, if required.
173 | 
174 | === "since `v2.0.1`"
175 | 
176 |     ```sh
177 |     showdown makehtml -i foo.md -o bar.html -c strikethrough -c emoji
178 |     ```
179 | 
180 | === "before `v2.0.1`"
181 | 
182 |     ```sh
183 |     showdown makehtml -i foo.md -o bar.html --strikethrough --emoji
184 |     ```
185 | 
186 | 
187 | You can specify any of the [supported options](available-options.md), and they will be passed to the converter.
188 | 
189 | The above commands are equivalent of doing:
190 | 
191 | ```js
192 | var conv = new showdown.Converter({strikethrough: true, emoji: true});
193 | ```
194 | 
195 | !!! warning ""
196 |     In the CLI tool, all the extra options are **disabled** by default. This is the opposite of what is defined for node and browser, where some options, like `ghCodeBlocks` are enabled (for backward compatibility and historical reasons).
197 | 


--------------------------------------------------------------------------------
/docs/compatibility.md:
--------------------------------------------------------------------------------
 1 | ## Browsers
 2 | 
 3 | Showdown has been tested successfully with:
 4 | 
 5 | *  Firefox 1.5 and 2.0
 6 | *  Chrome 12.0
 7 | *  Internet Explorer 6 and 7
 8 | *  Safari 2.0.4
 9 | *  Opera 8.54 and 9.10
10 | *  Netscape 8.1.2
11 | *  Konqueror 3.5.4
12 | 
13 | Generally, Showdown should work in any browser that supports ECMA 262 3rd Edition (JavaScript 1.5).
14 | 
15 | The converter might even work in things that aren't web browsers, like Acrobat. However, no promises.
16 | 
17 | ## Node.js
18 | 
19 | Showdown is intended to work on any supported Node.js version (see the [Node.js releases schedule](https://nodejs.org/en/about/releases/).
20 | 
21 | Previous versions may also be supported, but no accomodations are made to ensure this.


--------------------------------------------------------------------------------
/docs/configuration.md:
--------------------------------------------------------------------------------
 1 | You can change Showdown's default behavior via options. 
 2 | 
 3 | ## Set option
 4 | 
 5 | ### Globally
 6 | 
 7 | Setting an option globally affects all Showdown instances.
 8 | 
 9 | ```js
10 | showdown.setOption('optionKey', 'value');
11 | ```
12 | 
13 | ### Locally
14 | 
15 | Setting an option locally affects the specified Converter object only. You can set local options via:
16 | 
17 | === "Constructor"
18 | 
19 |     ```js
20 |     var converter = new showdown.Converter({optionKey: 'value'});
21 |     ```
22 | 
23 | === "setOption() method"
24 | 
25 |     ```js
26 |     var converter = new showdown.Converter();
27 |     converter.setOption('optionKey', 'value');
28 |     ```
29 | 
30 | ## Get option
31 | 
32 | Showdown provides both local and global methods to retrieve previously set options:
33 | 
34 | === "getOption()"
35 |     
36 |     ```js
37 |     // Global
38 |     var myOption = showdown.getOption('optionKey');
39 | 
40 |     //Local
41 |     var myOption = converter.getOption('optionKey');
42 |     ```
43 | 
44 | === "getOptions()"
45 | 
46 |     ```js
47 |     // Global
48 |     var showdownGlobalOptions = showdown.getOptions();
49 | 
50 |     //Local
51 |     var thisConverterSpecificOptions = converter.getOptions();
52 |     ```
53 | 
54 | ### Get default options
55 | 
56 | You can get Showdown's default options with:
57 | 
58 | ```js
59 | var defaultOptions = showdown.getDefaultOptions();
60 | ```


--------------------------------------------------------------------------------
/docs/create-extension.md:
--------------------------------------------------------------------------------
  1 | A Showdown extension is a function that returns an array of language or outputs extensions (henceforth called "sub-extensions"). 
  2 | 
  3 | ```js
  4 | var myext = function () {
  5 |   var myext1 = {
  6 |     type: 'lang',
  7 |     regex: /markdown/g,
  8 |     replace: 'showdown'
  9 |   };
 10 |   var myext2 = {
 11 |     /* extension code */
 12 |   };
 13 |   return [myext1, myext2];
 14 | }
 15 | ```
 16 | 
 17 | Each sub-extension (`myext1` and `myext2` in the example above) should be an object that defines the behavior of the corresponding sub-extension.
 18 | 
 19 | ## Sub-extension object properties
 20 | 
 21 | A sub-extension object should have a [`type` property](#type) that defines the type of the sub-extension, and either [`regex` and `replace` properties](#regex-and-replace) or a [`filter` property](#filter).
 22 | 
 23 | ### Type
 24 | 
 25 | **Type** is a **required** property that defines the nature of the corresponding sub-extensions. It takes one of the two values:
 26 | 
 27 | * **`lang`**: language extension to add new Markdown syntax to Showdown.
 28 | 
 29 |     `lang` extensions have the **highest priority** in the subparser order, so they are called after [escaping and normalizing](#escape-and-normalization) the input text and before calling any other subparser (or extension).
 30 | 
 31 |     !!! example "When to use `lang` type"
 32 | 
 33 |         For example, if you want the `^^youtube http://www.youtube.com/watch?v=oHg5SJYRHA0` syntax to automatically be rendered as an embedded YouTube video.
 34 | 
 35 | * **`output`**: output extension (or modifier) to alter the HTML output generated by Showdown.
 36 |  
 37 |     `output` extensions have the **lowest priority** in the subparser order, so they are called right before the cleanup step and after calling all other subparsers.
 38 | 
 39 |     !!! example "When to use `output` type"
 40 | 
 41 |         For example, if you want the `
` to become `
`. 42 | 43 | ### Regex and replace 44 | 45 | `regex`/`replace` properties are similar to the Javascript's `string.replace` function and work the same way: 46 | 47 | * `regex`: a `string` or a `RegExp` object. 48 | 49 | If `regex` is a `string`, it will automatically be assigned a `g` (global) modifier, that is, all matches of that string will be replaced. 50 | 51 | * `replace` a `string` or a `function`. 52 | 53 | If `replace` is a `string`, you can use the `$1` syntax for group substitution, exactly as if it were making use of `string.replace`. 54 | 55 | !!! example "Regex and replace example" 56 | 57 | In this example, all the occurrences of `markdown` will be replaced with `showndown`. 58 | 59 | ```js 60 | var myext = { 61 | type: 'lang', 62 | regex: /markdown/g, 63 | replace: 'showdown' 64 | }; 65 | ``` 66 | 67 | ### Filter 68 | 69 | Alternately, if you'd like to have more control over the modification process, you can use `filter` property. 70 | 71 | This property should be used as a function that acts as a callback. The callback should receive the following parameters: 72 | 73 | 1. `text`: the source text within the Showdown's engine. 74 | 1. `converter`: the full instance of the current Showdown's converter object. 75 | 1. `options`: the options used to initialize the converter 76 | 77 | !!! warning "" 78 | The filter function **should return the transformed text**. If it doesn't, it will fail **silently** and return an empty output. 79 | 80 | !!! example "Filter example" 81 | 82 | ```js 83 | var myext = { 84 | type: 'lang', 85 | filter: function (text, converter, options) { 86 | // ... do stuff to text ... 87 | return text; 88 | } 89 | }; 90 | ``` 91 | 92 | !!! warning "Use `filter` with care" 93 | 94 | Although Filter extensions are more powerful, they have a few pitfalls that you should keep in mind before using them, especially regarding the `converter` parameter. 95 | 96 | Since the `converter` parameter passed to the filter function is the fully initialized instance, any change made to it will be propagated outside the scope of the filter function and will remain there until a new converter instance is created. So, **it is not recommended to make ANY change to the converter object**. 97 | 98 | Another aspect is that if you call the `converter` recursively, it will call your extension itself at some point. It may lead to infinite recursion in some circumstances, and it's up to you to prevent this. A simple solution is to place a kind of safeguard to disable your extension if it's called more than x times: 99 | 100 | ```js 101 | var x = 0; 102 | var myext = { 103 | type: 'lang', 104 | filter: function (text, converter) { 105 | if (x < 3) { 106 | ++x; 107 | someSubText = converter.makeHtml(someSubText); 108 | } 109 | } 110 | }; 111 | ``` 112 | 113 | ## Register an extension 114 | 115 | 116 | To let Showdown know what extensions are available, you need to register them in the Showdown global object. 117 | 118 | To register an extension, call the `showdown.extension` function with two parameters: the first one is the extension name; the second one is the actual extension. 119 | 120 | ```js 121 | showdown.extension('myext', myext); 122 | ``` 123 | 124 | ## Test an extension 125 | 126 | The Showdown test runner is configured to automatically test cases for extensions. 127 | 128 | To add test cases for an extension: 129 | 130 | 1. Create a new folder under `./test/extensions` that matches with the name of the `.js` file in `./src/extensions`. 131 | 1. Place any test cases into the filter using the `md/html` format. These cases will automatically be executed when running tests. 132 | 133 | ## Additional information 134 | 135 | ### Escape and normalization 136 | 137 | Showdown performs the following escape/normalization: 138 | 139 | * Replaces `¨` (trema) with `¨T` 140 | * Replaces `$` (dollar sign) with `¨D` 141 | * Normalizes line endings (`\r`, `\r\n` are converted into `\n`) 142 | * Uses `\r` as a char placeholder 143 | 144 | !!! note "" 145 | This only applies to **language extensions** since these chars are unescaped before output extensions are run. 146 | 147 | !!! warning "" 148 | 149 | Keep in mind that these modifications happen **before language extensions** are run, so if your extension relies on any of those chars, you have to make the appropriate adjustments. 150 | 151 | 152 | ### Implementation concerns 153 | 154 | One of the concerns is maintaining both client-side and server-side compatibility. You can do this with a few lines of boilerplate code.: 155 | 156 | ```js 157 | (function (extension) { 158 | if (typeof showdown !== 'undefined') { 159 | // global (browser or node.js global) 160 | extension(showdown); 161 | } else if (typeof define === 'function' && define.amd) { 162 | // AMD 163 | define(['showdown'], extension); 164 | } else if (typeof exports === 'object') { 165 | // Node, CommonJS-like 166 | module.exports = extension(require('showdown')); 167 | } else { 168 | // showdown was not found so an error is thrown 169 | throw Error('Could not find showdown library'); 170 | } 171 | }(function (showdown) { 172 | // loading extension into showdown 173 | showdown.extension('myext', function () { 174 | var myext = { /* ... actual extension code ... */ }; 175 | return [myext]; 176 | }); 177 | })); 178 | ``` 179 | 180 | In the code above, the extension definition is wrapped in a self-executing function to prevent pollution of the global scope. It has another benefit of creating several scope layers that can be useful for interaction between sub-extensions global-wise or local-wise. 181 | 182 | It is also loaded conditionally to make it compatible with different loading mechanisms (such as browser, CommonJS, or AMD). 183 | -------------------------------------------------------------------------------- /docs/credits.md: -------------------------------------------------------------------------------- 1 | === "v.2" 2 | 3 | * [Estevão Santos](https://github.com/tivie) 4 | * [SyntaxRules](https://github.com/SyntaxRules) 5 | 6 | === "v.1" 7 | 8 | * [Estevão Santos](https://github.com/tivie) 9 | * [Pascal Deschênes](https://github.com/pdeschen) 10 | 11 | === "v.0" 12 | 13 | * [Corey Innis](http://github.com/coreyti) - Original GitHub project maintainer 14 | * [Remy Sharp](https://github.com/remy/) - CommonJS-compatibility and more 15 | * [Konstantin Käfer](https://github.com/kkaefer/) - CommonJS packaging 16 | * [Roger Braun](https://github.com/rogerbraun) - GitHub-style code blocks 17 | * [Dominic Tarr](https://github.com/dominictarr) - Documentation 18 | * [Cat Chen](https://github.com/CatChen) - Export fix 19 | * [Titus Stone](https://github.com/tstone) - Mocha tests, extension mechanism, bug fixes 20 | * [Rob Sutherland](https://github.com/roberocity) - The idea that lead to extensions 21 | * [Pavel Lang](https://github.com/langpavel) - Code cleanup 22 | * [Ben Combee](https://github.com/unwiredben) - Regex optimization 23 | * [Adam Backstrom](https://github.com/abackstrom) - WebKit bug fixes 24 | * [Pascal Deschênes](https://github.com/pdeschen) - Grunt support, extension fixes + additions, packaging improvements, documentation 25 | * [Estevão Santos](https://github.com/tivie) - Bug fixes and late maintainer 26 | * [Hannah Wolfe](https://github.com/ErisDS) - Bug fixes 27 | * [Alexandre Courtiol](https://github.com/acourtiol) - Bug fixes and build optimization 28 | * [Karthik Balakrishnan](https://github.com/torcellite) - Support for table alignment 29 | * [rheber](https://github.com/rheber) - CLI 30 | 31 | 32 | === "Original Project" 33 | 34 | * [John Gruber](http://daringfireball.net/projects/markdown/) - Author of Markdown 35 | * [John Fraser](http://attacklab.net/) - Author of Showdown 36 | -------------------------------------------------------------------------------- /docs/donations.md: -------------------------------------------------------------------------------- 1 | ShowdownJS is a **free** library and it will remain **free forever**. 2 | 3 | However, maintaining and improving the library costs time and money. 4 | 5 | If you like our work and find it useful, please donate through [PayPal](https://www.paypal.me/tiviesantos). 6 | 7 | :heart: :pray: Your contributions are greatly appreciated and will help us with the development of this awesome library. -------------------------------------------------------------------------------- /docs/event_system.md: -------------------------------------------------------------------------------- 1 | # Event System 2 | 3 | ## Introduction 4 | 5 | 6 | ## The Event Object 7 | 8 | 9 | ## Events 10 | 11 | Events are raised when a subparser is run (or about to be run). 12 | Within a subparser, the events always follow a certain order (sequence). For instance, **.before** events always run before **.captureStart**. 13 | Each subparser raises several events sequentially: 14 | 15 | 1. **.start**: **always runs** except it subparser is disabled 16 | 17 | Raised when the **subparser has started**, but no capturing or any modification to the text was done. 18 | 19 | **Always runs** (except if the subparser is deactivated through options). 20 | 21 | ***Properties***: 22 | 23 | | property | type | access | description | 24 | |----------|-----------|------------|--------------------------------------------------------------------| 25 | | input | string | read | The full text that was passed to the subparser | 26 | | output | string | write | The full text with modification that will be passed along the chain| 27 | | regexp | null | | | 28 | | matches | null | | | 29 | 30 | Usually you would want to use this event if you wish to change the input to the subparser 31 | 32 | 2. **.captureStart**: *might not be run*; 33 | 34 | Raised when a regex match is found and a capture was successful. Some normalization and modification 35 | of the regex captured groups might be performed. 36 | 37 | Might not be run if no regex match is found. 38 | 39 | ***Properties***: 40 | 41 | | property | type | access | description | 42 | |----------|-----------|------------|--------------------------------------------------------------------| 43 | | input | string | read | The captured text | 44 | | output | string | write | The text that will be passed to the subparser/other listeners | 45 | | regexp | RegExp | readonly | Regular Expression used to capture groups | 46 | | matches | object | read/write | Matches groups. Changes to this object are reflected in the output | 47 | 48 | Usually you would want to use this event if you wish to modify a certain subparser behavior. 49 | Exs: remove all title attributes from links; change indentation of code blocks; etc... 50 | 51 | 3. **.captureEnd**: *might not be run*; 52 | 53 | Raised after the modifications to the captured text are done but before the replacement is introduced in the document. 54 | 55 | Might not be run if no regex match is found. 56 | 57 | ***Properties***: 58 | 59 | | property | type | access | description | 60 | |------------|-----------|------------|--------------------------------------------------------------------------------| 61 | | input | string | read | The captured text | 62 | | output | string | write | The text that will be passed to the subparser/other listeners | 63 | | regexp | RegExp | readonly | Regular Expression used to capture groups | 64 | | matches | object | read/write | Keypairs of matches groups. Changes to this object are reflected in the output | 65 | | attributes | object | read/write | Attributes to add to the HTML output | 66 | 67 | 4. **.beforeHash**: *might not be run*; 68 | 69 | Raised before the output is hashed. 70 | 71 | Always run (except if the subparser was deactivated through options), even if no hashing is performed. 72 | 73 | ***Properties***: 74 | 75 | | property | type | access | description | 76 | |----------|------------|------------|--------------------------------------------------------------------| 77 | | input | string | read | The captured text | 78 | | output | string | write | The text that will be passed to the subparser/other listeners | 79 | | regexp | null | | | 80 | | matches | null | | | 81 | 82 | Usually you would want to use this event if you wish change the subparser output before it is hashed 83 | 84 | 5. **.end**: *always runs*; 85 | 86 | Raised when the subparser has finished its work and is about to exit. 87 | 88 | Always runs (except if the subparser is deactivated through options). 89 | 90 | ***Properties***: 91 | 92 | | property | type | access | description | 93 | |----------|-----------|------------|--------------------------------------------------------------------| 94 | | input | string | read | The partial/full text with the subparser modifications | 95 | | output | string | write | The text that will be passed to other subparsers | 96 | | regexp | null | | | 97 | | matches | null | | | 98 | 99 | Usually you would want to use this event if you wish change the subparser hashed output 100 | 101 | 102 | ### Special Events 103 | 104 | There are some special events that are useful for *"positioning"* a listener extension in the main chain of events. 105 | Usually these extensions introduce new syntax that, due to precedence 106 | These events are always guaranteed to be called, regardless of options or circumstances. 107 | 108 | 1. **.before_{subparserName}**: *always runs* 109 | 110 | Raised just before the **{subparserName} is about to be entered**. 111 | 112 | ***Properties***: 113 | 114 | | property | type | access | description | 115 | |----------|-----------|------------|--------------------------------------------------------------------| 116 | | input | string | read | The full text that was passed to the subparser | 117 | | output | string | write | The full text with modification that will be passed along the chain| 118 | | regexp | null | | | 119 | | matches | null | | | 120 | 121 | 2. **.after**.{subparserName}: *always runs*; 122 | 123 | Raised when the **{subparserName} has exited** and before the next one is called. 124 | 125 | ***Properties***: 126 | 127 | | property | type | access | description | 128 | |----------|-----------|------------|--------------------------------------------------------------------| 129 | | input | string | read | The partial/full text with the subparser modifications | 130 | | output | string | write | The text that will be passed to other subparsers | 131 | | regexp | null | | | 132 | | matches | null | | | 133 | 134 | 135 | ### Notes 136 | 137 | - There are 2 main differences between **before.{subparserName}** and **{subparserName}.start**. 138 | 139 | 1. **before.{subparserName}** is always guaranteed to be called, even if the subparser is disabled, 140 | while **{subparserName}.start** doesn't. 141 | 142 | ex: `makehtml.before.strikethrough` is always called even if the option `strikethrough` is false 143 | 144 | 2. **before.{subparserName}** is only raised once in a span context while **{subparserName}.start** is raised 145 | everytime **{subparserName}** is called. 146 | 147 | As a rule of thumb, 148 | 149 | ## Events List 150 | -------------------------------------------------------------------------------- /docs/extensions-list.md: -------------------------------------------------------------------------------- 1 | ## Official 2 | 3 | * [twitter-extension][1] - Adds support of Twitter usernames and hastags 4 | * [prettify-extension][2] - Adds [Google Prettify][3] hints to HTML output 5 | 6 | ## Community 7 | 8 | * [showdown-icon][4] - Adds support of Glyphicon and font-awesome into Markdown 9 | * [showdown-xss-filter][5] - Filters XSS, using leizongmin/js-xss 10 | * [showdown-toc][6] - Adds Table of Contents 11 | * [showdown-footnotes][7] - Adds simple footnotes 12 | * [katex-latex][8] - Displays math using KaTeX and LaTeX or AsciiMath 13 | 14 | !!! note "" 15 | If you have a Showdown extension you would like to add here, you can [raise an issue](https://github.com/showdownjs/showdown/issues). 16 | 17 | [1]: https://github.com/showdownjs/twitter-extension 18 | [2]: https://github.com/showdownjs/prettify-extension 19 | [3]: https://github.com/googlearchive/code-prettify 20 | [4]: https://github.com/dbtek/showdown-icon 21 | [5]: https://github.com/VisionistInc/showdown-xss-filter 22 | [6]: https://github.com/ravisorg/showdown-toc 23 | [7]: https://github.com/Kriegslustig/showdown-footnotes 24 | [8]: https://obedm503.github.io/showdown-katex 25 | -------------------------------------------------------------------------------- /docs/extensions.md: -------------------------------------------------------------------------------- 1 | Showdown allows you to load additional functionality via extensions. You can find a list of known Showdown extensions [here][ext-wiki]. 2 | 3 | You can also check the [boilerplate repo][boilerplate-repo], to create your own extension(s). 4 | 5 | ## Usage 6 | 7 | === "Server-side" 8 | 9 | ```js 10 | // Using a bundled extension 11 | var showdown = require('showdown'); 12 | var converter = new showdown.Converter({ extensions: ['twitter'] }); 13 | 14 | // Using a custom extension 15 | var mine = require('./custom-extensions/mine'); 16 | var converter = new showdown.Converter({ extensions: ['twitter', mine] }); 17 | ``` 18 | 19 | === "Client-side" 20 | 21 | ```js 22 | 23 | 24 | 25 | ``` 26 | 27 | === "CLI" 28 | 29 | In the CLI tool, use the [`-e` flag](/cli/#-e-extensions) to load an extension. 30 | 31 | ```sh 32 | showdown -e twitter -i foo.md -o bar.html 33 | ``` 34 | 35 | [ext-wiki]: https://github.com/showdownjs/showdown/wiki/extensions 36 | [boilerplate-repo]: https://github.com/showdownjs/extension-boilerplate -------------------------------------------------------------------------------- /docs/flavors.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | You can use _flavors_ (or presets) to set the preferred options automatically. In this way, Showdown behaves like popular Markdown flavors. 4 | 5 | Currently, the following flavors are available: 6 | 7 | * `original`: Original Markdown flavor as in [John Gruber's spec](https://daringfireball.net/projects/markdown/) 8 | * `vanilla`: Showdown base flavor (v1.3.1 onwards) 9 | * `github`: [GitHub Flavored Markdown, or GFM](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) 10 | 11 | ## Set flavor 12 | 13 | === "Globally" 14 | 15 | ```js 16 | showdown.setFlavor('github'); 17 | ``` 18 | 19 | === "Locally" 20 | 21 | ```js 22 | converter.setFlavor('github'); 23 | ``` -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Showdown documentation 2 | 3 | ![Showdown][sd-logo] 4 | 5 | ![Build Status: Linux](https://github.com/showdownjs/showdown/actions/workflows/node.linux.yml/badge.svg) 6 | ![Build Status: Windows](https://github.com/showdownjs/showdown/actions/workflows/node.win.yml/badge.svg) 7 | [![npm version](https://badge.fury.io/js/showdown.svg)](http://badge.fury.io/js/showdown) 8 | [![Bower version](https://badge.fury.io/bo/showdown.svg)](http://badge.fury.io/bo/showdown) 9 | [![Join the chat at https://gitter.im/showdownjs/showdown](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/showdownjs/showdown?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 10 | [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/tiviesantos) 11 | 12 | Showdown is a JavaScript Markdown to HTML converter, based on the original works by John Gruber. 13 | Showdown can be used on the client-side (in the browser) or server-side (with Node.js). 14 | 15 | ---- 16 | 17 | ## Live demo 18 | 19 | 20 | 21 | ## Who uses Showdown (or a fork) 22 | 23 | * [Antmarky](https://github.com/bandantonio/antmarky) 24 | * [GoogleCloudPlatform](https://github.com/GoogleCloudPlatform) 25 | * [Meteor](https://www.meteor.com/) 26 | * [StackExchange](http://stackexchange.com/) - forked as [PageDown](https://code.google.com/p/pagedown/) 27 | * [docular](https://github.com/Vertafore/docular) 28 | * [md-page](https://github.com/oscarmorrison/md-page) 29 | * [QCObjects](https://qcobjects.dev) 30 | * [and some others](https://www.npmjs.com/browse/depended/showdown) 31 | 32 | ## Installation 33 | 34 | To install Showdown, follow the instructions from the [Quickstart guide](quickstart.md). 35 | 36 | 37 | ## License 38 | 39 | ShowdownJS v 2.0 is release under the MIT version. 40 | 41 | Previous versions are release under BSD. 42 | 43 | [sd-logo]: https://raw.githubusercontent.com/showdownjs/logo/master/dist/logo.readme.png 44 | [wiki]: https://github.com/showdownjs/showdown/wiki 45 | [cli-wiki]: https://github.com/showdownjs/showdown/wiki/CLI-tool 46 | [definitely-typed]: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/showdown 47 | [xss-wiki]: https://github.com/showdownjs/showdown/wiki/Markdown's-XSS-Vulnerability-(and-how-to-mitigate-it) 48 | [ext-wiki]: https://github.com/showdownjs/showdown/wiki/extensions 49 | [coding-rules]: https://github.com/showdownjs/code-style/blob/master/README.md 50 | [ng-commit-guide]: https://github.com/showdownjs/code-style/blob/master/README.md#commit-message-convention 51 | [boilerplate-repo]: https://github.com/showdownjs/extension-boilerplate -------------------------------------------------------------------------------- /docs/integrations.md: -------------------------------------------------------------------------------- 1 | ## AngularJS 2 | 3 | ShowdownJS project provides seamless integration with AngularJS via a plugin. 4 | 5 | Check [`ng-showdown`](https://github.com/showdownjs/ngShowdown) repository for more information. 6 | 7 | ## TypeScript 8 | 9 | If you're using TypeScript, you may want to use the types from the [DefinitelyTyped][definitely-typed] repository. 10 | 11 | ## SystemJS/JSPM 12 | 13 | To integrate ShowdownJS with SystemJS, you can use a third-party [system-md plugin](https://github.com/guybedford/system-md). 14 | 15 | ## Vue.js 16 | 17 | To use ShowdownJS as a Vue component, you can check [vue-showdown](https://vue-showdown.js.org/). 18 | 19 | 20 | [definitely-typed]: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/showdown -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | To quickstart with Showdown, install it as a package (for server-side) or include it to your browser (client-side) via CDN: 2 | 3 | ## Installation 4 | 5 | ### Server-side 6 | 7 | === "npm" 8 | 9 | ``` 10 | npm install showdown 11 | ``` 12 | 13 | === "bower" 14 | 15 | ``` 16 | bower install showdown 17 | ``` 18 | 19 | === "NuGet" 20 | 21 | ``` 22 | PM> Install-Package showdownjs 23 | ``` 24 | 25 | More information about the package you can find on the [NuGet website](https://www.nuget.org/packages/showdownjs/). 26 | 27 | ### Client-side 28 | 29 | === "jsDelivr" 30 | 31 | ``` 32 | https://cdn.jsdelivr.net/npm/showdown@/dist/showdown.min.js 33 | ``` 34 | 35 | [Showndown page on jsDelivr](https://www.jsdelivr.com/package/npm/showdown) 36 | 37 | === "cdnjs" 38 | 39 | ``` 40 | https://cdnjs.cloudflare.com/ajax/libs/showdown//showdown.min.js 41 | ``` 42 | 43 | [Showndown page on cdnjs](https://cdnjs.com/libraries/showdown) 44 | 45 | === "unpkg" 46 | 47 | ``` 48 | https://unpkg.com/showdown/dist/showdown.min.js 49 | ``` 50 | 51 | [Showndown page on unpkg](https://unpkg.com/browse/showdown@latest/) 52 | 53 | !!! note "" 54 | Replace `` with an actual full length version you're interested in. For example, `2.0.3`. 55 | 56 | ## Usage 57 | 58 | Once installed, you can use Showndown according to the chosen method: 59 | 60 | ### Server-side 61 | 62 | !!! example "Node.js" 63 | 64 | === "code" 65 | 66 | ```js 67 | var showdown = require('showdown'), 68 | converter = new showdown.Converter(), 69 | text = '# hello, markdown!', 70 | html = converter.makeHtml(text); 71 | ``` 72 | 73 | === "output" 74 | 75 | ```html 76 |

hello, markdown!

77 | ``` 78 | 79 | ### Client-side 80 | 81 | !!! example "Browser" 82 | 83 | === "code" 84 | 85 | ```js 86 | var converter = new showdown.Converter(), 87 | text = '# hello, markdown!', 88 | html = converter.makeHtml(text); 89 | ``` 90 | 91 | === "output" 92 | 93 | ```html 94 |

hello, markdown!

95 | ``` 96 | 97 | !!! warning "Potential XSS vulnerabilities" 98 | Showdown doesn't sanitize the input since Markdown relies on it to parse certain features correctly into HTML. As a result, this may lead to potential XSS injection vulnerabilities. 99 | 100 | Please refer to the [Markdown's XSS vulnerability](xss.md) page for more information. 101 | 102 | ## Other installation methods 103 | 104 | ### Tarball 105 | 106 | You can download the latest tarball directly from [releases][releases]. 107 | 108 | ## Previous versions 109 | 110 | If you're looking for Showdown prior to version 1.0.0, you can find them in the [legacy branch][legacy-branch]. 111 | 112 | ## Changelog 113 | 114 | The full changelog is available [here][changelog]. 115 | 116 | [legacy-branch]: https://github.com/showdownjs/showdown/tree/legacy 117 | [releases]: https://github.com/showdownjs/showdown/releases 118 | [changelog]: https://github.com/showdownjs/showdown/blob/master/CHANGELOG.md -------------------------------------------------------------------------------- /docs/tutorials/add-default-class-to-html.md: -------------------------------------------------------------------------------- 1 | # Add default class for each HTML element 2 | 3 | Many people use CSS kits like Bootstrap, Semantic UI, or others that require default name classes for HTML elements: 4 | 5 | ```html 6 |

1st Heading

7 |

2nd Heading

8 |
    9 |
  • first item
  • 10 |
  • second item
  • 11 |
12 | ``` 13 | 14 | Showdown does not support this out-of-the-box. But you can create an extension for this: 15 | 16 | ```js 17 | const showdown = require('showdown'); 18 | 19 | const classMap = { 20 | h1: 'ui large header', 21 | h2: 'ui medium header', 22 | ul: 'ui list', 23 | li: 'ui item' 24 | } 25 | 26 | const bindings = Object.keys(classMap) 27 | .map(key => ({ 28 | type: 'output', 29 | regex: new RegExp(`<${key}(.*)>`, 'g'), 30 | replace: `<${key} class="${classMap[key]}" $1>` 31 | })); 32 | 33 | const conv = new showdown.Converter({ 34 | extensions: [...bindings] 35 | }); 36 | 37 | const text = ` 38 | # 1st Heading 39 | ## 2nd Heading 40 | 41 | - first item 42 | - second item 43 | `; 44 | ``` 45 | 46 | With this extension, the output will be as follows: 47 | 48 | ```html 49 | ​​​​​

1st Heading

​​​​​ 50 | ​​​​​

2nd Heading

​​​​​ 51 | ​​​​​
    ​​​​​ 52 | ​​​​​
  • first item
  • ​​​​​ 53 | ​​​​​
  • second item
  • ​​​​​ 54 | ​​​​​
​​​​​ 55 | ``` 56 | 57 | ## Credits 58 | 59 | * Initial creator: [@zusamann](https://github.com/zusamann), [(original issue)](https://github.com/showdownjs/showdown/issues/376). 60 | * Updated by [@Kameelridder](https://github.com/Kameelridder), [(original issue)](https://github.com/showdownjs/showdown/issues/509). 61 | -------------------------------------------------------------------------------- /docs/tutorials/index.md: -------------------------------------------------------------------------------- 1 | # Tutorials 2 | 3 | * [Add default class for each HTML element](add-default-class-to-html.md) 4 | * [Markdown editor with Showdown](markdown-editor-with-showdown.md) 5 | * [Use language and output extensions on the same block](use-both-extension-types-together.md) -------------------------------------------------------------------------------- /docs/tutorials/markdown-editor-with-showdown.md: -------------------------------------------------------------------------------- 1 | # Markdown editor with Showdown 2 | 3 | ## Introduction 4 | 5 | In this tutorial, you will create a simple in-browser Markdown editor using Showdown and some of its extensions. The purpose is to show how easy it is to include and configure Showdown in your project. 6 | 7 | The fully working example you can see in [Fiddle][1]. 8 | 9 | ## Step 1: Prepare project 10 | 11 | 1. Install [node.js](https://nodejs.org/en/). 12 | 1. Install project package management tool 13 | 14 | !!! info "" 15 | Showdown core library doesn't have any dependencies so the setup is pretty straightforward. However, you are strongly encouraged to use a package manager such as [**npm**](http://npmjs.com) or [**yarn**](https://yarnpkg.com) to manage project dependencies. 16 | 17 | To install package management tool: 18 | 19 | 1. Create a directory called `showdown-editor` and recreate the following structure: 20 | 21 | ``` 22 | showdown-editor 23 | ├── css 24 | │ └── style.css 25 | ├── js 26 | │ └── script.js 27 | └── index.html 28 | ``` 29 | 30 | 1. Initialize `package.json` file by running the following interactive console command: 31 | 32 | ``` 33 | npm init -y 34 | ``` 35 | 36 | This command creates `package.json` file in the root of the project folder, and populates the default content that you can change later if you wish. 37 | 38 | ## Step 2: Install Showdown 39 | 40 | Inside the `showdown-editor` directory, run the following command: 41 | 42 | ``` 43 | npm install showdown --save 44 | ``` 45 | 46 | This command will install `showdown` inside the `node_modules` directory and save `showdown` as a dependency in the `package.json` file. 47 | 48 | ## Step 3: Update project files 49 | 50 | Add the following content to the corresponding project files: 51 | 52 | === "index.html" 53 | 54 | ```html 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 88 |
89 | 90 |
91 |
92 | 93 | 94 | 95 | 96 | 97 | ``` 98 | 99 | !!! warning "" 100 | Please note how Showdown and the script file are included to the `index.html` via the `script` tag at the bottom of the file. 101 | 102 | === "style.css" 103 | 104 | ```css 105 | #sourceTA { 106 | display: block; 107 | } 108 | #targetDiv { 109 | border: 1px dashed #333333; 110 | width: 600px; 111 | height: 400px; 112 | } 113 | ``` 114 | 115 | === "script.js" 116 | 117 | ```js 118 | function run() { 119 | var text = document.getElementById('sourceTA').value, 120 | target = document.getElementById('targetDiv'), 121 | converter = new showdown.Converter(), 122 | html = converter.makeHtml(text); 123 | 124 | target.innerHTML = html; 125 | } 126 | ``` 127 | 128 | The `script.js` file is simple: when the `runBtn` button is clicked, the script gets the text of the textarea, passes it through Showdown to convert the markdown text into HTML. The resulting HTML is then put inside the `targetDiv`, replacing the previous content. 129 | 130 | ## Step 4: Check the result 131 | 132 | 1. Open your `index.html` file. You should see your editor with prefilled markdown text in the text area. 133 | 1. Click `Convert` button. You show see the text to be converted to HTML: 134 | 135 | ![](../assets/markdown-editor.png) 136 | 137 | The fully working example you can see in [Fiddle][1]. 138 | 139 | ## Conclusion 140 | 141 | Congratulations! :tada: You have successfully created a simple Markdown editor! 142 | 143 | [1]: http://jsfiddle.net/tivie/6bnpptkb/ -------------------------------------------------------------------------------- /docs/tutorials/use-both-extension-types-together.md: -------------------------------------------------------------------------------- 1 | # Use language and output extensions on the same block 2 | 3 | ## Overview 4 | 5 | Showdown allows you to define and use any number of extensions that act on the same block. These extensions can be executed sequentially or at different moments. 6 | 7 | This enables you to pre-parse/mark a block of text but defer any modifications for the last by using a combination of language and output extensions. 8 | 9 | This is useful if you, for example, don't want Showdown to parse the contents of your new language construct. 10 | 11 | ## Example 12 | 13 | Let's say you create an extension that captures everything between `%start%` and `%end%`. However, that content should not be modified by Showdown. Obviously, you can use `
` tags but that is beside the point.
14 | 
15 | Although Showdown doesn't have any flag to prevent parsing the content of an extension, the same effect can be easily achieved by using lang and output extensions together.
16 | 
17 | !!! example ""
18 |     The fully working example you can see in [Fiddle][1].
19 | 
20 | ### Code
21 | 
22 | [Create your extensions](../create-extension.md) with the following content:
23 | 
24 | ```js
25 | showdown.extension('myExt', function() {
26 |   var matches = [];
27 |   return [
28 |     { 
29 |       type: 'lang',
30 |       regex: /%start%([^]+?)%end%/gi,
31 |       replace: function(s, match) { 
32 |         matches.push(match);
33 |         var n = matches.length - 1;
34 |         return '%PLACEHOLDER' + n + '%';
35 |       }
36 |     },
37 |     {
38 |       type: 'output',
39 |       filter: function (text) {
40 |         for (var i=0; i< matches.length; ++i) {
41 |           var pat = '

%PLACEHOLDER' + i + '% *<\/p>'; 42 | text = text.replace(new RegExp(pat, 'gi'), matches[i]); 43 | } 44 | //reset array 45 | matches = []; 46 | return text; 47 | } 48 | } 49 | ] 50 | }); 51 | ``` 52 | 53 | In this example, you created a [`lang` extension](../create-extension.md#type) that: 54 | 55 | 1. Checks for the pseudo tags `%start%` and `%end%`. 56 | 1. Extracts everything in between the tags. 57 | 1. Saves the content between the tags in a variable. 58 | 1. Replaces the saved content with a placeholder to identify the exact position of the extracted text. 59 | 60 | and an [`output` extension](../create-extension.md#type) that replaces the placeholder with the saved content, once Showdown is finished parsing. 61 | 62 | [1]: http://jsfiddle.net/tivie/1rqr7xy8/ -------------------------------------------------------------------------------- /docs/xss.md: -------------------------------------------------------------------------------- 1 | # Markdown's XSS vulnerability 2 | 3 | ## Introduction 4 | 5 | Cross-Site Scripting (XSS) is a well-known technique to gain access to the private information of users on a website. The attacker injects spurious HTML content (a script) on the web page. This script can read the user’s cookies and do other malicious actions (like steal credentials). As a countermeasure, you should always filter user input for suspicious content. Showdown doesn’t include an XSS filter, so you must provide your own. But be careful in how you do it. 6 | 7 | ## Markdown is inherently unsafe 8 | 9 | Markdown syntax allows the inclusion of arbitrary HTML. For example, below is a perfectly valid Markdown: 10 | 11 | ```md 12 | This is a regular paragraph. 13 | 14 | 15 | 16 |
Foo
17 | 18 | This is another regular paragraph. 19 | ``` 20 | 21 | This means that an attacker could do something like this: 22 | 23 | ```md 24 | This is a regular paragraph. 25 | 26 | 27 | 28 | This is another regular paragraph. 29 | ``` 30 | 31 | While `alert('xss');` is hardly problematic (maybe just annoying) a real-world scenario might be a lot worse. Obviously, you can easily prevent this kind of this straightforward attack. For example, you can define a whitelist for Showdown that will contain a limited set of allowed HTML tags. However, an attacker can easily circumvent this "defense". 32 | 33 | ## Whitelist / blacklist can't prevent XSS 34 | 35 | Consider the following Markdown content: 36 | 37 | ```md 38 | hello *you* 39 | ``` 40 | 41 | As you can see, it's a link, nothing malicious about this. And `` tags are pretty innocuous, right? Showdown should definitely allow them. But what if the content is slightly altered, like this: 42 | 43 | ```md 44 | hello *you* 45 | ``` 46 | 47 | Now this is a lot more problematic. Once again, it's not that hard to filter Showdown's input to expunge problematic attributes (such as `href` in `` tags) of scripting attacks. In fact, a regular HTML XSS prevention library should catch this kind of straightforward attack. 48 | 49 | At this point you're probably thinking that the best way is to follow Stackoverflow's cue and disallow embedded HTML in Markdown. Unfortunately it's still not enough. 50 | 51 | ## Strip HTML tags is not enough 52 | 53 | Consider the following Markdown input: 54 | 55 | ```md 56 | [some text](javascript:alert('xss')) 57 | ``` 58 | 59 | Showdown will correctly parse this piece of Markdown input as: 60 | 61 | ```html 62 | some text 63 | ``` 64 | 65 | In this case, it was Markdown's syntax itself to create the dangerous link. HTML XSS filter cannot catch this. And unless you start striping dangerous words like *javascript* (which would make this article extremely hard to write), there's nothing you can really do to filter XSS attacks from your input. Things get even harder when you tightly mix HTML with Markdown. 66 | 67 | ## Mixed HTML/Markdown XSS attack 68 | 69 | Consider the following piece of Markdown: 70 | 71 | ```md 72 | > hello href="javascript:alert('xss')">*you* 74 | ``` 75 | 76 | If you apply an XSS filter to filter bad HTML in this Markdown input, the XSS filter, expecting HTML, will likely think the `` tag ends with the first character on the second line and will leave the text snippet untouched. It will probably fail to see that the `href="javascript:…"` is part of the `` element and leave it alone. But when Markdown converts this to HTML, you get this: 77 | 78 | ```html 79 |

80 |

hello you

82 |
83 | ``` 84 | 85 | After parsing with Markdown, the first `>` on the second line disappears because it was the blockquote marker in the Markdown blockquote syntax. As a result, you’ve got a link containing an XSS attack! 86 | 87 | Did Markdown generate the HTML? No, the HTML was already in plain sight in the input. The XSS filter couldn’t catch it because the input doesn’t follow HTML rules: it’s a mix of Markdown and HTML, and the filter doesn’t know a dime about Markdown. 88 | 89 | ## Mitigate XSS 90 | 91 | So, is it all lost? Not really. The answer is not to filter the *input* but rather the *output*. After the *input* text is converted into full-fledged HTML, you can reliably apply the correct XSS filters to remove any dangerous or malicious content. 92 | 93 | Also, client-side validations are not reliable. It should be a given, but in case you're wondering, you should (almost) never trust data sent by the client. If there's some critical operation you must perform on the data (such as XSS filtering), you should do it *SERVER-SIDE* not client-side. 94 | 95 | HTML XSS filtering libraries are useful here since they prevent most of the attacks. However, you should not use them blindly: a library can't predict all the contexts and situations your application may face. 96 | 97 | ## Conclusion 98 | 99 | Showdown tries to convert the input text as closely as possible, without any concerns for XSS attacks or malicious intent. So, the basic rules are: 100 | 101 | * **removing HTML entities from Markdown does not prevent XSS**. Markdown syntax can generate XSS attacks. 102 | * **XSS filtering should be done after Showdown has processed input, not before or during**. If you filter before, it will break some of Markdown’s features and will leave security holes. 103 | * **perform the necessary filtering server-side, not client-side**. XSS filtering libraries are useful but should not be used blindly. 104 | 105 | ## Disclaimer 106 | 107 | This page is based on the excellent article: ["Markdown and XSS"][1] by [Michel Fortin][2] 108 | 109 | [1]: https://michelf.ca/blog/2010/markdown-and-xss/ 110 | [2]: https://github.com/michelf 111 | -------------------------------------------------------------------------------- /karma.browserstack.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | // global config of your BrowserStack account 4 | browserStack: { 5 | username: process.env.BROWSERSTACK_USERNAME, 6 | accessKey: process.env.BROWSERSTACK_ACCESSKEY, 7 | project: process.env.BROWSERSTACK_PROJECT_NAME || 'showdown', 8 | build: process.env.BROWSERSTACK_BUILD_NAME || require('./package.json').version, 9 | name: process.env.COMMIT_MSG || 'Unit Testing' 10 | }, 11 | 12 | // define browsers 13 | customLaunchers: { 14 | bstack_chrome_windows: { 15 | base: 'BrowserStack', 16 | browser: 'chrome', 17 | browser_version: '49', 18 | os: 'Windows', 19 | os_version: '10' 20 | }, 21 | bstack_firefox_windows: { 22 | base: 'BrowserStack', 23 | browser: 'firefox', 24 | browser_version: '44', 25 | os: 'Windows', 26 | os_version: '10' 27 | }, 28 | bstack_edge_windows: { 29 | base: 'BrowserStack', 30 | browser: 'edge', 31 | browser_version: '15', 32 | os: 'Windows', 33 | os_version: '10' 34 | }, 35 | bstack_ie11_windows: { 36 | base: 'BrowserStack', 37 | browser: 'ie', 38 | browser_version: '11', 39 | os: 'Windows', 40 | os_version: '10' 41 | }, 42 | bstack_macos_safari: { 43 | base: 'BrowserStack', 44 | browser: 'safari', 45 | browser_version: '10.1', 46 | os: 'OS X', 47 | os_version: 'Sierra' 48 | }, 49 | bstack_iphoneX: { 50 | base: 'BrowserStack', 51 | browser: 'safari', 52 | os: 'ios', 53 | os_version: '11.0', 54 | device: 'iPhone X', 55 | real_mobile: true 56 | }, 57 | bstack_android: { 58 | base: 'BrowserStack', 59 | browser: 'chrome', 60 | os: 'android', 61 | os_version:'4.4', 62 | device: 'Samsung Galaxy Tab 4', 63 | realMobile: true 64 | } 65 | }, 66 | 67 | browsers: ['bstack_chrome_windows', 'bstack_firefox_windows', 'bstack_ie11_windows', 'bstack_edge_windows', 'bstack_iphoneX', 'bstack_macos_safari', 'bstack_android'], 68 | frameworks: ['mocha', 'chai'], 69 | reporters: ['dots', 'BrowserStack'], 70 | files: [ 71 | { pattern: '.build/showdown.js'}, 72 | { pattern: 'src/options.js'}, 73 | // tests 74 | { pattern: 'test/unit/showdown*.js' } 75 | //{ pattern: 'test/functional/showdown*.js' }, 76 | ], 77 | singleRun: true, 78 | concurrency: Infinity 79 | }); 80 | }; 81 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | client: { 4 | captureConsole: true 5 | }, 6 | browserConsoleLogOptions: { 7 | level: 'log', 8 | format: '%b %T: %m', 9 | terminal: true 10 | }, 11 | logLevel: config.LOG_LOG, 12 | frameworks: ['mocha', 'chai'], 13 | files: [ 14 | { pattern: '.build/showdown.js'}, 15 | { pattern: 'src/options.js'}, 16 | // tests 17 | { pattern: 'test/unit/showdown*.js' }, 18 | { pattern: 'test/functional/showdown*.js' }, 19 | ], 20 | reporters: ['progress'], 21 | port: 9876, // karma web server port 22 | colors: true, 23 | browsers: ['ChromeHeadless', 'FirefoxHeadless', 'jsdom'], 24 | autoWatch: false, 25 | singleRun: true, // Karma captures browsers, runs the tests and exits 26 | //concurrency: Infinity, 27 | customLaunchers: { 28 | 'FirefoxHeadless': { 29 | base: 'Firefox', 30 | flags: [ 31 | '-headless', 32 | ] 33 | } 34 | }, 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Showdown documentation 2 | site_description: Showdown is a JavaScript Markdown to HTML converter 3 | 4 | theme: 5 | name: material 6 | logo: http://showdownjs.com/apple-touch-icon.png 7 | favicon: http://showdownjs.com/apple-touch-icon.png 8 | icon: 9 | repo: fontawesome/brands/github 10 | features: 11 | - navigation.tabs 12 | 13 | markdown_extensions: 14 | - admonition 15 | - pymdownx.superfences 16 | - pymdownx.tabbed: 17 | alternate_style: true 18 | - pymdownx.tasklist 19 | - pymdownx.emoji: 20 | emoji_index: !!python/name:materialx.emoji.twemoji 21 | emoji_generator: !!python/name:materialx.emoji.to_svg 22 | 23 | extra_css: 24 | - assets/extra.css 25 | 26 | repo_url: https://github.com/showdownjs/showdown 27 | repo_name: showdownjs/showdown 28 | site_dir: public 29 | 30 | nav: 31 | - Home: 32 | - Introduction: index.md 33 | - Donations: donations.md 34 | - Credits: credits.md 35 | - Quickstart: 36 | - Quickstart: quickstart.md 37 | - Showdown's Markdown syntax: markdown-syntax.md 38 | - Compatibility: compatibility.md 39 | - Configuration: 40 | - Showdown options: configuration.md 41 | - Available options: available-options.md 42 | - Flavors: flavors.md 43 | - CLI: cli.md 44 | - Integrations: integrations.md 45 | - Extensions: 46 | - Overview: extensions.md 47 | - Create an extension: create-extension.md 48 | - List of known extensions: extensions-list.md 49 | - Tutorials: tutorials/index.md -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "showdown", 3 | "version": "3.0.0-alpha", 4 | "description": "A Markdown to HTML converter written in Javascript", 5 | "author": "Estevão Santos", 6 | "homepage": "http://showdownjs.com/", 7 | "keywords": [ 8 | "markdown", 9 | "converter" 10 | ], 11 | "contributors": [ 12 | "John Gruber", 13 | "John Fraser", 14 | "Corey Innis", 15 | "Remy Sharp", 16 | "Konstantin Käfer", 17 | "Roger Braun", 18 | "Dominic Tarr", 19 | "Cat Chen", 20 | "Titus Stone", 21 | "Rob Sutherland", 22 | "Pavel Lang", 23 | "Ben Combee", 24 | "Adam Backstrom", 25 | "Pascal Deschênes", 26 | "Estevão Santos" 27 | ], 28 | "funding": { 29 | "type": "individual", 30 | "url": "https://www.paypal.me/tiviesantos" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/showdownjs/showdown.git", 35 | "web": "https://github.com/showdownjs/showdown" 36 | }, 37 | "license": "MIT", 38 | "main": "./dist/showdown.js", 39 | "scripts": { 40 | "test": "grunt test" 41 | }, 42 | "bin": { 43 | "showdown": "bin/showdown.js" 44 | }, 45 | "files": [ 46 | "bin", 47 | "dist" 48 | ], 49 | "devDependencies": { 50 | "chai": "*", 51 | "chai-match": "*", 52 | "grunt": "^1.4.1", 53 | "grunt-contrib-clean": "^2.0.0", 54 | "grunt-contrib-concat": "^2.0.0", 55 | "grunt-contrib-jshint": "^3.1.0", 56 | "grunt-contrib-uglify": "^5.0.1", 57 | "grunt-conventional-changelog": "^6.1.0", 58 | "grunt-conventional-github-releaser": "^1.0.0", 59 | "grunt-endline": "^0.7.0", 60 | "grunt-eslint": "^24.0.0", 61 | "grunt-mocha-test": "^0.13.3", 62 | "grunt-simple-mocha": "^0.4.0", 63 | "karma": "^6.3.17", 64 | "karma-browserstack-launcher": "^1.6.0", 65 | "karma-chai": "^0.1.0", 66 | "karma-chrome-launcher": "^3.1.1", 67 | "karma-firefox-launcher": "^2.1.2", 68 | "karma-jsdom-launcher": "^12.0.0", 69 | "karma-mocha": "^2.0.1", 70 | "load-grunt-tasks": "^5.1.0", 71 | "performance-now": "^2.1.0", 72 | "quiet-grunt": "^0.2.0", 73 | "semver-sort": "^1.0.0", 74 | "sinon": "*", 75 | "source-map-support": "^0.5.21" 76 | }, 77 | "dependencies": { 78 | "commander": "^9.0.0", 79 | "jsdom": "^19.0.0" 80 | }, 81 | "overrides": { 82 | "minimist": "^1.2.6" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/cli/cli.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tivie 3 | */ 4 | var fs = require('fs'), 5 | path = require('path'), 6 | Command = require('commander').Command, 7 | program = new Command(), 8 | path1 = path.resolve(__dirname + '/../dist/showdown.js'), 9 | path2 = path.resolve(__dirname + '/../../.build/showdown.js'), 10 | showdown, 11 | version; 12 | 13 | // require shodown. We use conditional loading for each use case 14 | if (fs.existsSync(path1)) { 15 | // production. File lives in bin directory 16 | showdown = require(path1); 17 | version = require(path.resolve(__dirname + '/../package.json')).version; 18 | } else if (fs.existsSync(path2)) { 19 | // testing envo, uses the concatenated stuff for testing 20 | showdown = require(path2); 21 | version = require(path.resolve(__dirname + '/../../package.json')).version; 22 | } else { 23 | // cold testing (manual) of cli.js in the src file. We load the dist file 24 | showdown = require('../../dist/showdown'); 25 | version = require('../../package.json'); 26 | } 27 | 28 | 29 | program 30 | .name('showdown') 31 | .description('CLI to Showdownjs markdown parser v' + version) 32 | .version(version) 33 | .usage(' [options]') 34 | .option('-q, --quiet', 'Quiet mode. Only print errors') 35 | .option('-m, --mute', 'Mute mode. Does not print anything'); 36 | 37 | program.command('makehtml') 38 | .description('Converts markdown into html') 39 | 40 | .addHelpText('after', '\n\nExamples:') 41 | .addHelpText('after', ' showdown makehtml -i Reads from stdin and outputs to stdout') 42 | .addHelpText('after', ' showdown makehtml -i foo.md -o bar.html Reads \'foo.md\' and writes to \'bar.html\'') 43 | .addHelpText('after', ' showdown makehtml -i --flavor="github" Parses stdin using GFM style') 44 | 45 | .addHelpText('after', '\nNote for windows users:') 46 | .addHelpText('after', 'When reading from stdin, use option -u to set the proper encoding or run `chcp 65001` prior to calling showdown cli to set the command line to utf-8') 47 | 48 | .option('-i, --input [file]', 'Input source. Usually a md file. If omitted or empty, reads from stdin. Windows users see note below.', true) 49 | .option('-o, --output [file]', 'Output target. Usually a html file. If omitted or empty, writes to stdout', true) 50 | .option('-u, --encoding ', 'Sets the input encoding', 'utf8') 51 | .option('-y, --output-encoding ', 'Sets the output encoding', 'utf8') 52 | .option('-a, --append', 'Append data to output instead of overwriting. Ignored if writing to stdout', false) 53 | .option('-e, --extensions ', 'Load the specified extensions. Should be valid paths to node compatible extensions') 54 | .option('-p, --flavor ', 'Run with a predetermined flavor of options. Default is vanilla', 'vanilla') 55 | .option('-c, --config ', 'Enables showdown makehtml parser config options. Overrides flavor') 56 | .option('--config-help', 'Shows configuration options for showdown parser') 57 | .action(makehtmlCommand); 58 | 59 | program.parse(); 60 | 61 | 62 | // 63 | // HELPER FUCNTIONS 64 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 65 | 66 | /** 67 | * Messenger helper object to the CLI 68 | * @param {string} writeMode 69 | * @param {boolean} supress 70 | * @param {boolean} mute 71 | * @constructor 72 | */ 73 | function Messenger (writeMode, supress, mute) { 74 | 'use strict'; 75 | writeMode = writeMode || 'stderr'; 76 | supress = (!!supress || !!mute); 77 | mute = !!mute; 78 | this._print = (writeMode === 'stdout') ? console.log : console.error; 79 | 80 | this.errorExit = function (e) { 81 | if (!mute) { 82 | console.error('ERROR: ' + e.message); 83 | console.error('Run \'showdown -h\' for help'); 84 | } 85 | process.exit(1); 86 | }; 87 | 88 | this.okExit = function () { 89 | if (!mute) { 90 | this._print('\n'); 91 | this._print('DONE!'); 92 | } 93 | process.exit(0); 94 | }; 95 | 96 | this.printMsg = function (msg) { 97 | if (supress || mute || !msg) { 98 | return; 99 | } 100 | this._print(msg); 101 | }; 102 | 103 | this.printError = function (msg) { 104 | if (mute) { 105 | return; 106 | } 107 | console.error(msg); 108 | }; 109 | 110 | } 111 | 112 | /** 113 | * Helper function to show Showdown Options 114 | */ 115 | function showShowdownOptions () { 116 | 'use strict'; 117 | var showdownOptions = showdown.getDefaultOptions(false); 118 | console.log('\nshowdown makehtml config options:'); 119 | // show showdown options 120 | for (var sopt in showdownOptions) { 121 | if (showdownOptions.hasOwnProperty(sopt)) { 122 | console.log(' ' + sopt + ':', '[default=' + showdownOptions[sopt].defaultValue + ']',showdownOptions[sopt].describe); 123 | } 124 | } 125 | console.log('\n\nExample: showdown makehtml -c openLinksInNewWindow ghMentions ghMentionsLink="https://google.com"'); 126 | } 127 | 128 | /** 129 | * Helper function to parse showdown options 130 | * @param {{}} configOptions 131 | * @param {{}} defaultOptions 132 | * @returns {{}} 133 | */ 134 | function parseShowdownOptions (configOptions, defaultOptions) { 135 | 'use strict'; 136 | var shOpt = defaultOptions; 137 | 138 | // first prepare passed options 139 | if (configOptions) { 140 | for (var i = 0; i < configOptions.length; ++i) { 141 | var opt = configOptions[i], 142 | key = configOptions[i], 143 | val = true; 144 | if (/=/.test(opt)) { 145 | key = opt.split('=')[0]; 146 | val = opt.split('=')[1]; 147 | } 148 | shOpt[key] = val; 149 | } 150 | } 151 | return shOpt; 152 | } 153 | 154 | /** 155 | * Reads stdin 156 | * @returns {string} 157 | */ 158 | function readFromStdIn (encoding) { 159 | 'use strict'; 160 | /* 161 | // aparently checking the size of stdin is unreliable so we just won't test 162 | var size = fs.fstatSync(process.stdin.fd).size; 163 | if (size <= 0) { 164 | throw new Error('Could not read from stdin, reason: stdin is empty'); 165 | } 166 | */ 167 | encoding = encoding || 'utf8'; 168 | try { 169 | return fs.readFileSync(process.stdin.fd, encoding).toString(); 170 | } catch (e) { 171 | throw new Error('Could not read from stdin, reason: ' + e.message); 172 | } 173 | } 174 | 175 | /** 176 | * Reads from a file 177 | * @param {string} file Filepath to dile 178 | * @param {string} encoding Encoding of the file 179 | * @returns {Buffer} 180 | */ 181 | function readFromFile (file, encoding) { 182 | 'use strict'; 183 | try { 184 | return fs.readFileSync(file, encoding); 185 | } catch (err) { 186 | throw new Error('Could not read from file ' + file + ', reason: ' + err.message); 187 | } 188 | } 189 | 190 | /** 191 | * Writes to stdout 192 | * @param {string} html 193 | * @returns {boolean} 194 | */ 195 | function writeToStdOut (html) { 196 | 'use strict'; 197 | if (!process.stdout.write(html)) { 198 | throw new Error('Could not write to StdOut'); 199 | } 200 | } 201 | 202 | /** 203 | * Writes to file 204 | * @param {string} html HTML to write 205 | * @param {string} file Filepath 206 | * @param {boolean} append If the result should be appended 207 | */ 208 | function writeToFile (html, file, append) { 209 | 'use strict'; 210 | // If a flag is passed, it means we should append instead of overwriting. 211 | // Only works with files, obviously 212 | var write = (append) ? fs.appendFileSync : fs.writeFileSync; 213 | try { 214 | write(file, html); 215 | } catch (err) { 216 | throw new Error('Could not write to file ' + file + ', readon: ' + err.message); 217 | } 218 | } 219 | 220 | /** 221 | * makehtml command 222 | * @param {{}} options 223 | * @param {Command} cmd 224 | */ 225 | function makehtmlCommand (options, cmd) { 226 | 'use strict'; 227 | 228 | // show configuration options for showdown helper if configHelp was passed 229 | if (options.configHelp) { 230 | showShowdownOptions(); 231 | return; 232 | } 233 | 234 | var quiet = !!(cmd.parent._optionValues.quiet), 235 | mute = !!(cmd.parent._optionValues.mute), 236 | readMode = (!options.input || options.input === '' || options.input === true) ? 'stdin' : 'file', 237 | writeMode = (!options.output || options.output === '' || options.output === true) ? 'stdout' : 'file', 238 | msgMode = (writeMode === 'file') ? 'stdout' : 'stderr', 239 | // initiate Messenger helper, can maybe be replaced with commanderjs internal stuff 240 | messenger = new Messenger(msgMode, quiet, mute), 241 | defaultOptions = showdown.getDefaultOptions(true), 242 | md, html; 243 | 244 | // deal with flavor first since config flag overrides flavor individual options 245 | if (options.flavor) { 246 | messenger.printMsg('Enabling flavor ' + options.flavor + '...'); 247 | defaultOptions = showdown.getFlavorOptions(options.flavor); 248 | if (!defaultOptions) { 249 | messenger.errorExit(new Error('Flavor ' + options.flavor + ' is not recognised')); 250 | return; 251 | } 252 | messenger.printMsg('OK!'); 253 | } 254 | // store config options in the options.config as an object 255 | options.config = parseShowdownOptions(options.config, defaultOptions); 256 | 257 | // print enabled options 258 | for (var o in options.config) { 259 | if (options.config.hasOwnProperty(o) && options.config[o] === true) { 260 | messenger.printMsg('Enabling option ' + o); 261 | } 262 | } 263 | 264 | // initialize the converter 265 | messenger.printMsg('\nInitializing converter...'); 266 | var converter; 267 | try { 268 | converter = new showdown.Converter(options.config); 269 | } catch (e) { 270 | messenger.errorExit(e); 271 | return; 272 | } 273 | messenger.printMsg('OK!'); 274 | 275 | // load extensions 276 | if (options.extensions) { 277 | messenger.printMsg('\nLoading extensions...'); 278 | for (var i = 0; i < options.extensions.length; ++i) { 279 | try { 280 | messenger.printMsg(options.extensions[i]); 281 | var ext = require(options.extensions[i]); 282 | converter.addExtension(ext, options.extensions[i]); 283 | messenger.printMsg(options.extensions[i] + ' loaded...'); 284 | } catch (e) { 285 | messenger.printError('ERROR: Could not load extension ' + options.extensions[i] + '. Reason:'); 286 | messenger.errorExit(e); 287 | } 288 | } 289 | } 290 | 291 | messenger.printMsg('...'); 292 | // read the input 293 | messenger.printMsg('Reading data from ' + readMode + '...'); 294 | 295 | if (readMode === 'stdin') { 296 | try { 297 | md = readFromStdIn(options.encoding); 298 | } catch (err) { 299 | messenger.errorExit(err); 300 | return; 301 | } 302 | } else { 303 | try { 304 | md = readFromFile(options.input, options.encoding); 305 | } catch (err) { 306 | messenger.errorExit(err); 307 | return; 308 | } 309 | } 310 | 311 | // process the input 312 | messenger.printMsg('Parsing markdown...'); 313 | html = converter.makeHtml(md); 314 | 315 | // write the output 316 | messenger.printMsg('Writing data to ' + writeMode + '...'); 317 | if (writeMode === 'stdout') { 318 | try { 319 | writeToStdOut(html); 320 | } catch (err) { 321 | messenger.errorExit(err); 322 | return; 323 | } 324 | } else { 325 | try { 326 | writeToFile(html, options.output, options.append); 327 | } catch (err) { 328 | messenger.errorExit(err); 329 | return; 330 | } 331 | } 332 | messenger.okExit(); 333 | } 334 | -------------------------------------------------------------------------------- /src/loader.js: -------------------------------------------------------------------------------- 1 | var root = this; 2 | 3 | // AMD Loader 4 | if (typeof define === 'function' && define.amd) { 5 | define(function () { 6 | 'use strict'; 7 | return showdown; 8 | }); 9 | 10 | // CommonJS/nodeJS Loader 11 | } else if (typeof module !== 'undefined' && module.exports) { 12 | module.exports = showdown; 13 | 14 | // Regular Browser loader 15 | } else { 16 | root.showdown = showdown; 17 | } 18 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Tivie on 13-07-2015. 3 | */ 4 | 5 | function getDefaultOpts (simple) { 6 | 'use strict'; 7 | 8 | var defaultOptions = { 9 | omitExtraWLInCodeBlocks: { 10 | defaultValue: false, 11 | describe: 'Omit the default extra whiteline added to code blocks', 12 | type: 'boolean' 13 | }, 14 | noHeaderId: { 15 | defaultValue: false, 16 | describe: 'Turn on/off generated header id', 17 | type: 'boolean' 18 | }, 19 | prefixHeaderId: { 20 | defaultValue: false, 21 | describe: 'Add a prefix to the generated header ids. Passing a string will prefix that string to the header id. Setting to true will add a generic \'section-\' prefix', 22 | type: 'string' 23 | }, 24 | rawPrefixHeaderId: { 25 | defaultValue: false, 26 | describe: 'Setting this option to true will prevent showdown from modifying the prefix. This might result in malformed IDs (if, for instance, the " char is used in the prefix)', 27 | type: 'boolean' 28 | }, 29 | ghCompatibleHeaderId: { 30 | defaultValue: false, 31 | describe: 'Generate header ids compatible with github style (spaces are replaced with dashes, a bunch of non alphanumeric chars are removed)', 32 | type: 'boolean' 33 | }, 34 | rawHeaderId: { 35 | defaultValue: false, 36 | describe: 'Remove only spaces, \' and " from generated header ids (including prefixes), replacing them with dashes (-). WARNING: This might result in malformed ids', 37 | type: 'boolean' 38 | }, 39 | headerLevelStart: { 40 | defaultValue: false, 41 | describe: 'The header blocks level start', 42 | type: 'integer' 43 | }, 44 | parseImgDimensions: { 45 | defaultValue: false, 46 | describe: 'Turn on/off image dimension parsing', 47 | type: 'boolean' 48 | }, 49 | simplifiedAutoLink: { 50 | defaultValue: false, 51 | describe: 'Turn on/off GFM autolink style', 52 | type: 'boolean' 53 | }, 54 | literalMidWordUnderscores: { 55 | defaultValue: false, 56 | describe: 'Parse midword underscores as literal underscores', 57 | type: 'boolean' 58 | }, 59 | literalMidWordAsterisks: { 60 | defaultValue: false, 61 | describe: 'Parse midword asterisks as literal asterisks', 62 | type: 'boolean' 63 | }, 64 | strikethrough: { 65 | defaultValue: false, 66 | describe: 'Turn on/off strikethrough support', 67 | type: 'boolean' 68 | }, 69 | tables: { 70 | defaultValue: false, 71 | describe: 'Turn on/off tables support', 72 | type: 'boolean' 73 | }, 74 | tablesHeaderId: { 75 | defaultValue: false, 76 | describe: 'Add an id to table headers', 77 | type: 'boolean' 78 | }, 79 | ghCodeBlocks: { 80 | defaultValue: true, 81 | describe: 'Turn on/off GFM fenced code blocks support', 82 | type: 'boolean' 83 | }, 84 | tasklists: { 85 | defaultValue: false, 86 | describe: 'Turn on/off GFM tasklist support', 87 | type: 'boolean' 88 | }, 89 | smoothLivePreview: { 90 | defaultValue: false, 91 | describe: 'Prevents weird effects in live previews due to incomplete input', 92 | type: 'boolean' 93 | }, 94 | smartIndentationFix: { 95 | defaultValue: false, 96 | describe: 'Tries to smartly fix indentation in es6 strings', 97 | type: 'boolean' 98 | }, 99 | disableForced4SpacesIndentedSublists: { 100 | defaultValue: false, 101 | describe: 'Disables the requirement of indenting nested sublists by 4 spaces', 102 | type: 'boolean' 103 | }, 104 | simpleLineBreaks: { 105 | defaultValue: false, 106 | describe: 'Parses simple line breaks as
(GFM Style)', 107 | type: 'boolean' 108 | }, 109 | requireSpaceBeforeHeadingText: { 110 | defaultValue: false, 111 | describe: 'Makes adding a space between `#` and the header text mandatory (GFM Style)', 112 | type: 'boolean' 113 | }, 114 | ghMentions: { 115 | defaultValue: false, 116 | describe: 'Enables github @mentions', 117 | type: 'boolean' 118 | }, 119 | ghMentionsLink: { 120 | defaultValue: 'https://github.com/{u}', 121 | describe: 'Changes the link generated by @mentions. Only applies if ghMentions option is enabled.', 122 | type: 'string' 123 | }, 124 | encodeEmails: { 125 | defaultValue: true, 126 | describe: 'Encode e-mail addresses through the use of Character Entities, transforming ASCII e-mail addresses into its equivalent decimal entities', 127 | type: 'boolean' 128 | }, 129 | openLinksInNewWindow: { 130 | defaultValue: false, 131 | describe: 'Open all links in new windows', 132 | type: 'boolean' 133 | }, 134 | backslashEscapesHTMLTags: { 135 | defaultValue: false, 136 | describe: 'Support for HTML Tag escaping. ex: \
foo\
', 137 | type: 'boolean' 138 | }, 139 | emoji: { 140 | defaultValue: false, 141 | describe: 'Enable emoji support. Ex: `this is a :smile: emoji`', 142 | type: 'boolean' 143 | }, 144 | underline: { 145 | defaultValue: false, 146 | describe: 'Enable support for underline. Syntax is double or triple underscores: `__underline word__`. With this option enabled, underscores no longer parses into `` and ``', 147 | type: 'boolean' 148 | }, 149 | ellipsis: { 150 | defaultValue: true, 151 | describe: 'Replaces three dots with the ellipsis unicode character', 152 | type: 'boolean' 153 | }, 154 | completeHTMLDocument: { 155 | defaultValue: false, 156 | describe: 'Outputs a complete html document, including ``, `` and `` tags', 157 | type: 'boolean' 158 | }, 159 | metadata: { 160 | defaultValue: false, 161 | describe: 'Enable support for document metadata (defined at the top of the document between `«««` and `»»»` or between `---` and `---`).', 162 | type: 'boolean' 163 | }, 164 | splitAdjacentBlockquotes: { 165 | defaultValue: false, 166 | describe: 'Split adjacent blockquote blocks', 167 | type: 'boolean' 168 | }, 169 | moreStyling: { 170 | defaultValue: false, 171 | describe: 'Adds some useful styling css classes in the generated html', 172 | type: 'boolean' 173 | }, 174 | relativePathBaseUrl: { 175 | defaultValue: false, 176 | describe: 'Prepends a base URL to relative paths', 177 | type: 'string' 178 | }, 179 | }; 180 | if (simple === false) { 181 | return JSON.parse(JSON.stringify(defaultOptions)); 182 | } 183 | var ret = {}; 184 | for (var opt in defaultOptions) { 185 | if (defaultOptions.hasOwnProperty(opt)) { 186 | ret[opt] = defaultOptions[opt].defaultValue; 187 | } 188 | } 189 | return ret; 190 | } 191 | 192 | function allOptionsOn () { 193 | 'use strict'; 194 | var options = getDefaultOpts(true), 195 | ret = {}; 196 | for (var opt in options) { 197 | if (options.hasOwnProperty(opt)) { 198 | ret[opt] = true; 199 | } 200 | } 201 | return ret; 202 | } 203 | -------------------------------------------------------------------------------- /src/showdown.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Tivie on 06-01-2015. 3 | */ 4 | // Private properties 5 | var showdown = {}, 6 | parsers = {}, 7 | extensions = {}, 8 | globalOptions = getDefaultOpts(true), 9 | setFlavor = 'vanilla', 10 | flavor = { 11 | github: { 12 | omitExtraWLInCodeBlocks: true, 13 | simplifiedAutoLink: true, 14 | literalMidWordUnderscores: true, 15 | strikethrough: true, 16 | tables: true, 17 | tablesHeaderId: true, 18 | ghCodeBlocks: true, 19 | tasklists: true, 20 | disableForced4SpacesIndentedSublists: true, 21 | simpleLineBreaks: true, 22 | requireSpaceBeforeHeadingText: true, 23 | ghCompatibleHeaderId: true, 24 | ghMentions: true, 25 | backslashEscapesHTMLTags: true, 26 | emoji: true, 27 | splitAdjacentBlockquotes: true 28 | }, 29 | original: { 30 | noHeaderId: true, 31 | ghCodeBlocks: false 32 | }, 33 | ghost: { 34 | omitExtraWLInCodeBlocks: true, 35 | parseImgDimensions: true, 36 | simplifiedAutoLink: true, 37 | literalMidWordUnderscores: true, 38 | strikethrough: true, 39 | tables: true, 40 | tablesHeaderId: true, 41 | ghCodeBlocks: true, 42 | tasklists: true, 43 | smoothLivePreview: true, 44 | simpleLineBreaks: true, 45 | requireSpaceBeforeHeadingText: true, 46 | ghMentions: false, 47 | encodeEmails: true 48 | }, 49 | vanilla: getDefaultOpts(true), 50 | allOn: allOptionsOn() 51 | }; 52 | 53 | /** 54 | * helper namespace 55 | * @type {{}} 56 | */ 57 | showdown.helper = {}; 58 | 59 | /** 60 | * TODO LEGACY SUPPORT CODE 61 | * @type {{}} 62 | */ 63 | showdown.extensions = {}; 64 | 65 | /** 66 | * Set a global option 67 | * @static 68 | * @param {string} key 69 | * @param {*} value 70 | * @returns {showdown} 71 | */ 72 | showdown.setOption = function (key, value) { 73 | 'use strict'; 74 | globalOptions[key] = value; 75 | return this; 76 | }; 77 | 78 | /** 79 | * Get a global option 80 | * @static 81 | * @param {string} key 82 | * @returns {*} 83 | */ 84 | showdown.getOption = function (key) { 85 | 'use strict'; 86 | return globalOptions[key]; 87 | }; 88 | 89 | /** 90 | * Get the global options 91 | * @static 92 | * @returns {{}} 93 | */ 94 | showdown.getOptions = function () { 95 | 'use strict'; 96 | return globalOptions; 97 | }; 98 | 99 | /** 100 | * Reset global options to the default values 101 | * @static 102 | */ 103 | showdown.resetOptions = function () { 104 | 'use strict'; 105 | globalOptions = getDefaultOpts(true); 106 | }; 107 | 108 | /** 109 | * Set the flavor showdown should use as default 110 | * @param {string} name 111 | */ 112 | showdown.setFlavor = function (name) { 113 | 'use strict'; 114 | if (!flavor.hasOwnProperty(name)) { 115 | throw Error(name + ' flavor was not found'); 116 | } 117 | showdown.resetOptions(); 118 | var preset = flavor[name]; 119 | setFlavor = name; 120 | for (var option in preset) { 121 | if (preset.hasOwnProperty(option)) { 122 | globalOptions[option] = preset[option]; 123 | } 124 | } 125 | }; 126 | 127 | /** 128 | * Get the currently set flavor 129 | * @returns {string} 130 | */ 131 | showdown.getFlavor = function () { 132 | 'use strict'; 133 | return setFlavor; 134 | }; 135 | 136 | /** 137 | * Get the options of a specified flavor. Returns undefined if the flavor was not found 138 | * @param {string} name Name of the flavor 139 | * @returns {{}|undefined} 140 | */ 141 | showdown.getFlavorOptions = function (name) { 142 | 'use strict'; 143 | if (flavor.hasOwnProperty(name)) { 144 | return flavor[name]; 145 | } 146 | }; 147 | 148 | /** 149 | * Get the default options 150 | * @static 151 | * @param {boolean} [simple=true] 152 | * @returns {{}} 153 | */ 154 | showdown.getDefaultOptions = function (simple) { 155 | 'use strict'; 156 | return getDefaultOpts(simple); 157 | }; 158 | 159 | /** 160 | * Get or set a subParser 161 | * 162 | * subParser(name) - Get a registered subParser 163 | * subParser(name, func) - Register a subParser 164 | * @static 165 | * @param {string} name 166 | * @param {function} [func] 167 | * @returns {*} 168 | */ 169 | showdown.subParser = function (name, func) { 170 | 'use strict'; 171 | if (showdown.helper.isString(name)) { 172 | if (typeof func !== 'undefined') { 173 | parsers[name] = func; 174 | } else { 175 | if (parsers.hasOwnProperty(name)) { 176 | return parsers[name]; 177 | } else { 178 | throw Error('SubParser named ' + name + ' not registered!'); 179 | } 180 | } 181 | } else { 182 | throw Error('showdown.subParser function first argument must be a string (the name of the subparser)'); 183 | } 184 | }; 185 | 186 | /** 187 | * Gets or registers an extension 188 | * @static 189 | * @param {string} name 190 | * @param {object|object[]|function=} ext 191 | * @returns {*} 192 | */ 193 | showdown.extension = function (name, ext) { 194 | 'use strict'; 195 | 196 | if (!showdown.helper.isString(name)) { 197 | throw Error('Extension \'name\' must be a string'); 198 | } 199 | 200 | name = showdown.helper.stdExtName(name); 201 | 202 | // Getter 203 | if (showdown.helper.isUndefined(ext)) { 204 | if (!extensions.hasOwnProperty(name)) { 205 | throw Error('Extension named ' + name + ' is not registered!'); 206 | } 207 | return extensions[name]; 208 | 209 | // Setter 210 | } else { 211 | // Expand extension if it's wrapped in a function 212 | if (typeof ext === 'function') { 213 | ext = ext(); 214 | } 215 | 216 | // Ensure extension is an array 217 | if (!showdown.helper.isArray(ext)) { 218 | ext = [ext]; 219 | } 220 | 221 | var validExtension = validate(ext, name); 222 | 223 | if (validExtension.valid) { 224 | extensions[name] = ext; 225 | } else { 226 | throw Error(validExtension.error); 227 | } 228 | } 229 | }; 230 | 231 | /** 232 | * Gets all extensions registered 233 | * @returns {{}} 234 | */ 235 | showdown.getAllExtensions = function () { 236 | 'use strict'; 237 | return extensions; 238 | }; 239 | 240 | /** 241 | * Remove an extension 242 | * @param {string} name 243 | */ 244 | showdown.removeExtension = function (name) { 245 | 'use strict'; 246 | delete extensions[name]; 247 | }; 248 | 249 | /** 250 | * Removes all extensions 251 | */ 252 | showdown.resetExtensions = function () { 253 | 'use strict'; 254 | extensions = {}; 255 | }; 256 | 257 | /** 258 | * Validate extension 259 | * @param {array} extension 260 | * @param {string} name 261 | * @returns {{valid: boolean, error: string}} 262 | */ 263 | function validate (extension, name) { 264 | 'use strict'; 265 | 266 | var errMsg = (name) ? 'Error in ' + name + ' extension->' : 'Error in unnamed extension', 267 | ret = { 268 | valid: true, 269 | error: '' 270 | }; 271 | 272 | if (!showdown.helper.isArray(extension)) { 273 | extension = [extension]; 274 | } 275 | 276 | for (var i = 0; i < extension.length; ++i) { 277 | var baseMsg = errMsg + ' sub-extension ' + i + ': ', 278 | ext = extension[i]; 279 | if (typeof ext !== 'object') { 280 | ret.valid = false; 281 | ret.error = baseMsg + 'must be an object, but ' + typeof ext + ' given'; 282 | return ret; 283 | } 284 | 285 | if (!showdown.helper.isString(ext.type)) { 286 | ret.valid = false; 287 | ret.error = baseMsg + 'property "type" must be a string, but ' + typeof ext.type + ' given'; 288 | return ret; 289 | } 290 | 291 | var type = ext.type = ext.type.toLowerCase(); 292 | 293 | // normalize extension type 294 | if (type === 'language') { 295 | type = ext.type = 'lang'; 296 | } 297 | 298 | if (type === 'html') { 299 | type = ext.type = 'output'; 300 | } 301 | 302 | if (type !== 'lang' && type !== 'output' && type !== 'listener') { 303 | ret.valid = false; 304 | ret.error = baseMsg + 'type ' + type + ' is not recognized. Valid values: "lang/language", "output/html" or "listener"'; 305 | return ret; 306 | } 307 | 308 | if (type === 'listener') { 309 | if (showdown.helper.isUndefined(ext.listeners)) { 310 | ret.valid = false; 311 | ret.error = baseMsg + '. Extensions of type "listener" must have a property called "listeners"'; 312 | return ret; 313 | } 314 | } else { 315 | if (showdown.helper.isUndefined(ext.filter) && showdown.helper.isUndefined(ext.regex)) { 316 | ret.valid = false; 317 | ret.error = baseMsg + type + ' extensions must define either a "regex" property or a "filter" method'; 318 | return ret; 319 | } 320 | } 321 | 322 | if (ext.listeners) { 323 | if (typeof ext.listeners !== 'object') { 324 | ret.valid = false; 325 | ret.error = baseMsg + '"listeners" property must be an object but ' + typeof ext.listeners + ' given'; 326 | return ret; 327 | } 328 | for (var ln in ext.listeners) { 329 | if (ext.listeners.hasOwnProperty(ln)) { 330 | if (typeof ext.listeners[ln] !== 'function') { 331 | ret.valid = false; 332 | ret.error = baseMsg + '"listeners" property must be an hash of [event name]: [callback]. listeners.' + ln + 333 | ' must be a function but ' + typeof ext.listeners[ln] + ' given'; 334 | return ret; 335 | } 336 | } 337 | } 338 | } 339 | 340 | if (ext.filter) { 341 | if (typeof ext.filter !== 'function') { 342 | ret.valid = false; 343 | ret.error = baseMsg + '"filter" must be a function, but ' + typeof ext.filter + ' given'; 344 | return ret; 345 | } 346 | } else if (ext.regex) { 347 | if (showdown.helper.isString(ext.regex)) { 348 | ext.regex = new RegExp(ext.regex, 'g'); 349 | } 350 | if (!(ext.regex instanceof RegExp)) { 351 | ret.valid = false; 352 | ret.error = baseMsg + '"regex" property must either be a string or a RegExp object, but ' + typeof ext.regex + ' given'; 353 | return ret; 354 | } 355 | if (showdown.helper.isUndefined(ext.replace)) { 356 | ret.valid = false; 357 | ret.error = baseMsg + '"regex" extensions must implement a replace string or function'; 358 | return ret; 359 | } 360 | } 361 | } 362 | return ret; 363 | } 364 | 365 | /** 366 | * Validate extension 367 | * @param {object} ext 368 | * @returns {boolean} 369 | */ 370 | showdown.validateExtension = function (ext) { 371 | 'use strict'; 372 | 373 | var validateExtension = validate(ext, null); 374 | if (!validateExtension.valid) { 375 | console.warn(validateExtension.error); 376 | return false; 377 | } 378 | return true; 379 | }; 380 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/blockGamut.js: -------------------------------------------------------------------------------- 1 | /** 2 | * These are all the transformations that form block-level 3 | * tags like paragraphs, headers, and list items. 4 | */ 5 | showdown.subParser('makehtml.blockGamut', function (text, options, globals) { 6 | 'use strict'; 7 | 8 | text = globals.converter._dispatch('makehtml.blockGamut.before', text, options, globals).getText(); 9 | 10 | // we parse blockquotes first so that we can have headings and hrs 11 | // inside blockquotes 12 | text = showdown.subParser('makehtml.blockQuotes')(text, options, globals); 13 | text = showdown.subParser('makehtml.headers')(text, options, globals); 14 | 15 | // Do Horizontal Rules: 16 | text = showdown.subParser('makehtml.horizontalRule')(text, options, globals); 17 | 18 | text = showdown.subParser('makehtml.lists')(text, options, globals); 19 | text = showdown.subParser('makehtml.codeBlocks')(text, options, globals); 20 | text = showdown.subParser('makehtml.tables')(text, options, globals); 21 | 22 | // We already ran _HashHTMLBlocks() before, in Markdown(), but that 23 | // was to escape raw HTML in the original Markdown source. This time, 24 | // we're escaping the markup we've just created, so that we don't wrap 25 | //

tags around block-level tags. 26 | text = showdown.subParser('makehtml.hashHTMLBlocks')(text, options, globals); 27 | text = showdown.subParser('makehtml.paragraphs')(text, options, globals); 28 | 29 | text = globals.converter._dispatch('makehtml.blockGamut.after', text, options, globals).getText(); 30 | 31 | return text; 32 | }); 33 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/blockQuotes.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makehtml.blockQuotes', function (text, options, globals) { 2 | 'use strict'; 3 | 4 | text = globals.converter._dispatch('makehtml.blockQuotes.before', text, options, globals).getText(); 5 | 6 | // add a couple extra lines after the text and endtext mark 7 | text = text + '\n\n'; 8 | 9 | var rgx = /(^ {0,3}>[ \t]?.+\n(.+\n)*\n*)+/gm; 10 | 11 | if (options.splitAdjacentBlockquotes) { 12 | rgx = /^ {0,3}>[\s\S]*?(?:\n\n)/gm; 13 | } 14 | 15 | text = text.replace(rgx, function (bq) { 16 | // attacklab: hack around Konqueror 3.5.4 bug: 17 | // "----------bug".replace(/^-/g,"") == "bug" 18 | bq = bq.replace(/^[ \t]*>[ \t]?/gm, ''); // trim one level of quoting 19 | 20 | // attacklab: clean up hack 21 | bq = bq.replace(/¨0/g, ''); 22 | 23 | bq = bq.replace(/^[ \t]+$/gm, ''); // trim whitespace-only lines 24 | bq = showdown.subParser('makehtml.githubCodeBlocks')(bq, options, globals); 25 | bq = showdown.subParser('makehtml.blockGamut')(bq, options, globals); // recurse 26 | 27 | bq = bq.replace(/(^|\n)/g, '$1 '); 28 | // These leading spaces screw with

 content, so we need to fix that:
29 |     bq = bq.replace(/(\s*
[^\r]+?<\/pre>)/gm, function (wholeMatch, m1) {
30 |       var pre = m1;
31 |       // attacklab: hack around Konqueror 3.5.4 bug:
32 |       pre = pre.replace(/^  /mg, '¨0');
33 |       pre = pre.replace(/¨0/g, '');
34 |       return pre;
35 |     });
36 | 
37 |     return showdown.subParser('makehtml.hashBlock')('
\n' + bq + '\n
', options, globals); 38 | }); 39 | 40 | text = globals.converter._dispatch('makehtml.blockQuotes.after', text, options, globals).getText(); 41 | return text; 42 | }); 43 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/codeBlocks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Process Markdown `
` blocks.
 3 |  */
 4 | showdown.subParser('makehtml.codeBlocks', function (text, options, globals) {
 5 |   'use strict';
 6 | 
 7 |   text = globals.converter._dispatch('makehtml.codeBlocks.before', text, options, globals).getText();
 8 | 
 9 |   // sentinel workarounds for lack of \A and \Z, safari\khtml bug
10 |   text += '¨0';
11 | 
12 |   var pattern = /(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=¨0))/g;
13 |   text = text.replace(pattern, function (wholeMatch, m1, m2) {
14 |     var codeblock = m1,
15 |         nextChar = m2,
16 |         end = '\n';
17 | 
18 |     codeblock = showdown.subParser('makehtml.outdent')(codeblock, options, globals);
19 |     codeblock = showdown.subParser('makehtml.encodeCode')(codeblock, options, globals);
20 |     codeblock = showdown.subParser('makehtml.detab')(codeblock, options, globals);
21 |     codeblock = codeblock.replace(/^\n+/g, ''); // trim leading newlines
22 |     codeblock = codeblock.replace(/\n+$/g, ''); // trim trailing newlines
23 | 
24 |     if (options.omitExtraWLInCodeBlocks) {
25 |       end = '';
26 |     }
27 | 
28 |     codeblock = '
' + codeblock + end + '
'; 29 | 30 | return showdown.subParser('makehtml.hashBlock')(codeblock, options, globals) + nextChar; 31 | }); 32 | 33 | // strip sentinel 34 | text = text.replace(/¨0/, ''); 35 | 36 | text = globals.converter._dispatch('makehtml.codeBlocks.after', text, options, globals).getText(); 37 | return text; 38 | }); 39 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/codeSpans.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * * Backtick quotes are used for spans. 4 | * 5 | * * You can use multiple backticks as the delimiters if you want to 6 | * include literal backticks in the code span. So, this input: 7 | * 8 | * Just type ``foo `bar` baz`` at the prompt. 9 | * 10 | * Will translate to: 11 | * 12 | *

Just type foo `bar` baz at the prompt.

13 | * 14 | * There's no arbitrary limit to the number of backticks you 15 | * can use as delimters. If you need three consecutive backticks 16 | * in your code, use four for delimiters, etc. 17 | * 18 | * * You can use spaces to get literal backticks at the edges: 19 | * 20 | * ... type `` `bar` `` ... 21 | * 22 | * Turns to: 23 | * 24 | * ... type `bar` ... 25 | */ 26 | showdown.subParser('makehtml.codeSpans', function (text, options, globals) { 27 | 'use strict'; 28 | 29 | text = globals.converter._dispatch('makehtml.codeSpans.before', text, options, globals).getText(); 30 | 31 | if (typeof (text) === 'undefined') { 32 | text = ''; 33 | } 34 | text = text.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm, 35 | function (wholeMatch, m1, m2, m3) { 36 | var c = m3; 37 | c = c.replace(/^([ \t]*)/g, ''); // leading whitespace 38 | c = c.replace(/[ \t]*$/g, ''); // trailing whitespace 39 | c = showdown.subParser('makehtml.encodeCode')(c, options, globals); 40 | c = m1 + '' + c + ''; 41 | c = showdown.subParser('makehtml.hashHTMLSpans')(c, options, globals); 42 | return c; 43 | } 44 | ); 45 | 46 | text = globals.converter._dispatch('makehtml.codeSpans.after', text, options, globals).getText(); 47 | return text; 48 | }); 49 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/completeHTMLDocument.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a full HTML document from the processed markdown 3 | */ 4 | showdown.subParser('makehtml.completeHTMLDocument', function (text, options, globals) { 5 | 'use strict'; 6 | 7 | if (!options.completeHTMLDocument) { 8 | return text; 9 | } 10 | 11 | text = globals.converter._dispatch('makehtml.completeHTMLDocument.before', text, options, globals).getText(); 12 | 13 | var doctype = 'html', 14 | doctypeParsed = '\n', 15 | title = '', 16 | charset = '\n', 17 | lang = '', 18 | metadata = ''; 19 | 20 | if (typeof globals.metadata.parsed.doctype !== 'undefined') { 21 | doctypeParsed = '\n'; 22 | doctype = globals.metadata.parsed.doctype.toString().toLowerCase(); 23 | if (doctype === 'html' || doctype === 'html5') { 24 | charset = ''; 25 | } 26 | } 27 | 28 | for (var meta in globals.metadata.parsed) { 29 | if (globals.metadata.parsed.hasOwnProperty(meta)) { 30 | switch (meta.toLowerCase()) { 31 | case 'doctype': 32 | break; 33 | 34 | case 'title': 35 | title = '' + globals.metadata.parsed.title + '\n'; 36 | break; 37 | 38 | case 'charset': 39 | if (doctype === 'html' || doctype === 'html5') { 40 | charset = '\n'; 41 | } else { 42 | charset = '\n'; 43 | } 44 | break; 45 | 46 | case 'language': 47 | case 'lang': 48 | lang = ' lang="' + globals.metadata.parsed[meta] + '"'; 49 | metadata += '\n'; 50 | break; 51 | 52 | default: 53 | metadata += '\n'; 54 | } 55 | } 56 | } 57 | 58 | text = doctypeParsed + '\n\n' + title + charset + metadata + '\n\n' + text.trim() + '\n\n'; 59 | 60 | text = globals.converter._dispatch('makehtml.completeHTMLDocument.after', text, options, globals).getText(); 61 | return text; 62 | }); 63 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/detab.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert all tabs to spaces 3 | */ 4 | showdown.subParser('makehtml.detab', function (text, options, globals) { 5 | 'use strict'; 6 | text = globals.converter._dispatch('makehtml.detab.before', text, options, globals).getText(); 7 | 8 | // expand first n-1 tabs 9 | text = text.replace(/\t(?=\t)/g, ' '); // g_tab_width 10 | 11 | // replace the nth with two sentinels 12 | text = text.replace(/\t/g, '¨A¨B'); 13 | 14 | // use the sentinel to anchor our regex so it doesn't explode 15 | text = text.replace(/¨B(.+?)¨A/g, function (wholeMatch, m1) { 16 | var leadingText = m1, 17 | numSpaces = 4 - leadingText.length % 4; // g_tab_width 18 | 19 | // there *must* be a better way to do this: 20 | for (var i = 0; i < numSpaces; i++) { 21 | leadingText += ' '; 22 | } 23 | 24 | return leadingText; 25 | }); 26 | 27 | // clean up sentinels 28 | text = text.replace(/¨A/g, ' '); // g_tab_width 29 | text = text.replace(/¨B/g, ''); 30 | 31 | text = globals.converter._dispatch('makehtml.detab.after', text, options, globals).getText(); 32 | return text; 33 | }); 34 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/ellipsis.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makehtml.ellipsis', function (text, options, globals) { 2 | 'use strict'; 3 | 4 | if (!options.ellipsis) { 5 | return text; 6 | } 7 | 8 | text = globals.converter._dispatch('makehtml.ellipsis.before', text, options, globals).getText(); 9 | 10 | text = text.replace(/\.\.\./g, '…'); 11 | 12 | text = globals.converter._dispatch('makehtml.ellipsis.after', text, options, globals).getText(); 13 | 14 | return text; 15 | }); 16 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/emoji.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Turn emoji codes into emojis 3 | * 4 | * List of supported emojis: https://github.com/showdownjs/showdown/wiki/Emojis 5 | */ 6 | showdown.subParser('makehtml.emoji', function (text, options, globals) { 7 | 'use strict'; 8 | 9 | if (!options.emoji) { 10 | return text; 11 | } 12 | 13 | text = globals.converter._dispatch('makehtml.emoji.before', text, options, globals).getText(); 14 | 15 | var emojiRgx = /:([\S]+?):/g; 16 | 17 | text = text.replace(emojiRgx, function (wm, emojiCode) { 18 | if (showdown.helper.emojis.hasOwnProperty(emojiCode)) { 19 | return showdown.helper.emojis[emojiCode]; 20 | } 21 | return wm; 22 | }); 23 | 24 | text = globals.converter._dispatch('makehtml.emoji.after', text, options, globals).getText(); 25 | 26 | return text; 27 | }); 28 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/encodeAmpsAndAngles.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Smart processing for ampersands and angle brackets that need to be encoded. 3 | */ 4 | showdown.subParser('makehtml.encodeAmpsAndAngles', function (text, options, globals) { 5 | 'use strict'; 6 | text = globals.converter._dispatch('makehtml.encodeAmpsAndAngles.before', text, options, globals).getText(); 7 | 8 | // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: 9 | // http://bumppo.net/projects/amputator/ 10 | text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g, '&'); 11 | 12 | // Encode naked <'s 13 | text = text.replace(/<(?![a-z\/?$!])/gi, '<'); 14 | 15 | // Encode < 16 | text = text.replace(/ 19 | text = text.replace(/>/g, '>'); 20 | 21 | text = globals.converter._dispatch('makehtml.encodeAmpsAndAngles.after', text, options, globals).getText(); 22 | return text; 23 | }); 24 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/encodeBackslashEscapes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the string, with after processing the following backslash escape sequences. 3 | * 4 | * attacklab: The polite way to do this is with the new escapeCharacters() function: 5 | * 6 | * text = escapeCharacters(text,"\\",true); 7 | * text = escapeCharacters(text,"`*_{}[]()>#+-.!",true); 8 | * 9 | * ...but we're sidestepping its use of the (slow) RegExp constructor 10 | * as an optimization for Firefox. This function gets called a LOT. 11 | */ 12 | showdown.subParser('makehtml.encodeBackslashEscapes', function (text, options, globals) { 13 | 'use strict'; 14 | text = globals.converter._dispatch('makehtml.encodeBackslashEscapes.before', text, options, globals).getText(); 15 | 16 | text = text.replace(/\\(\\)/g, showdown.helper.escapeCharactersCallback); 17 | text = text.replace(/\\([`*_{}\[\]()>#+.!~=|:-])/g, showdown.helper.escapeCharactersCallback); 18 | 19 | text = globals.converter._dispatch('makehtml.encodeBackslashEscapes.after', text, options, globals).getText(); 20 | return text; 21 | }); 22 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/encodeCode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Encode/escape certain characters inside Markdown code runs. 3 | * The point is that in code, these characters are literals, 4 | * and lose their special Markdown meanings. 5 | */ 6 | showdown.subParser('makehtml.encodeCode', function (text, options, globals) { 7 | 'use strict'; 8 | 9 | text = globals.converter._dispatch('makehtml.encodeCode.before', text, options, globals).getText(); 10 | 11 | // Encode all ampersands; HTML entities are not 12 | // entities within a Markdown code span. 13 | text = text 14 | .replace(/&/g, '&') 15 | // Do the angle bracket song and dance: 16 | .replace(//g, '>') 18 | // Now, escape characters that are magic in Markdown: 19 | .replace(/([*_{}\[\]\\=~-])/g, showdown.helper.escapeCharactersCallback); 20 | 21 | text = globals.converter._dispatch('makehtml.encodeCode.after', text, options, globals).getText(); 22 | return text; 23 | }); 24 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/escapeSpecialCharsWithinTagAttributes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Within tags -- meaning between < and > -- encode [\ ` * _ ~ =] so they 3 | * don't conflict with their use in Markdown for code, italics and strong. 4 | */ 5 | showdown.subParser('makehtml.escapeSpecialCharsWithinTagAttributes', function (text, options, globals) { 6 | 'use strict'; 7 | text = globals.converter._dispatch('makehtml.escapeSpecialCharsWithinTagAttributes.before', text, options, globals).getText(); 8 | 9 | // Build a regex to find HTML tags. 10 | var tags = /<\/?[a-z\d_:-]+(?:[\s]+[\s\S]+?)?>/gi, 11 | comments = /-]|-[^>])(?:[^-]|-[^-])*)--)>/gi; 12 | 13 | text = text.replace(tags, function (wholeMatch) { 14 | return wholeMatch 15 | .replace(/(.)<\/?code>(?=.)/g, '$1`') 16 | .replace(/([\\`*_~=|])/g, showdown.helper.escapeCharactersCallback); 17 | }); 18 | 19 | text = text.replace(comments, function (wholeMatch) { 20 | return wholeMatch 21 | .replace(/([\\`*_~=|])/g, showdown.helper.escapeCharactersCallback); 22 | }); 23 | 24 | text = globals.converter._dispatch('makehtml.escapeSpecialCharsWithinTagAttributes.after', text, options, globals).getText(); 25 | return text; 26 | }); 27 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/githubCodeBlocks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Handle github codeblocks prior to running HashHTML so that 3 | * HTML contained within the codeblock gets escaped properly 4 | * Example: 5 | * ```ruby 6 | * def hello_world(x) 7 | * puts "Hello, #{x}" 8 | * end 9 | * ``` 10 | */ 11 | showdown.subParser('makehtml.githubCodeBlocks', function (text, options, globals) { 12 | 'use strict'; 13 | 14 | // early exit if option is not enabled 15 | if (!options.ghCodeBlocks) { 16 | return text; 17 | } 18 | 19 | text = globals.converter._dispatch('makehtml.githubCodeBlocks.before', text, options, globals).getText(); 20 | 21 | text += '¨0'; 22 | 23 | text = text.replace(/(?:^|\n) {0,3}(```+|~~~+) *([^\n\t`~]*)\n([\s\S]*?)\n {0,3}\1/g, function (wholeMatch, delim, language, codeblock) { 24 | var end = (options.omitExtraWLInCodeBlocks) ? '' : '\n'; 25 | 26 | // if the language has spaces followed by some other chars, according to the spec we should just ignore everything 27 | // after the first space 28 | language = language.trim().split(' ')[0]; 29 | 30 | // First parse the github code block 31 | codeblock = showdown.subParser('makehtml.encodeCode')(codeblock, options, globals); 32 | codeblock = showdown.subParser('makehtml.detab')(codeblock, options, globals); 33 | codeblock = codeblock.replace(/^\n+/g, ''); // trim leading newlines 34 | codeblock = codeblock.replace(/\n+$/g, ''); // trim trailing whitespace 35 | 36 | codeblock = '
' + codeblock + end + '
'; 37 | 38 | codeblock = showdown.subParser('makehtml.hashBlock')(codeblock, options, globals); 39 | 40 | // Since GHCodeblocks can be false positives, we need to 41 | // store the primitive text and the parsed text in a global var, 42 | // and then return a token 43 | return '\n\n¨G' + (globals.ghCodeBlocks.push({text: wholeMatch, codeblock: codeblock}) - 1) + 'G\n\n'; 44 | }); 45 | 46 | // attacklab: strip sentinel 47 | text = text.replace(/¨0/, ''); 48 | 49 | return globals.converter._dispatch('makehtml.githubCodeBlocks.after', text, options, globals).getText(); 50 | }); 51 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/hashBlock.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makehtml.hashBlock', function (text, options, globals) { 2 | 'use strict'; 3 | text = globals.converter._dispatch('makehtml.hashBlock.before', text, options, globals).getText(); 4 | text = text.replace(/(^\n+|\n+$)/g, ''); 5 | text = '\n\n¨K' + (globals.gHtmlBlocks.push(text) - 1) + 'K\n\n'; 6 | text = globals.converter._dispatch('makehtml.hashBlock.after', text, options, globals).getText(); 7 | return text; 8 | }); 9 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/hashCodeTags.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Hash and escape elements that should not be parsed as markdown 3 | */ 4 | showdown.subParser('makehtml.hashCodeTags', function (text, options, globals) { 5 | 'use strict'; 6 | text = globals.converter._dispatch('makehtml.hashCodeTags.before', text, options, globals).getText(); 7 | 8 | var repFunc = function (wholeMatch, match, left, right) { 9 | var codeblock = left + showdown.subParser('makehtml.encodeCode')(match, options, globals) + right; 10 | return '¨C' + (globals.gHtmlSpans.push(codeblock) - 1) + 'C'; 11 | }; 12 | 13 | // Hash naked 14 | text = showdown.helper.replaceRecursiveRegExp(text, repFunc, ']*>', '', 'gim'); 15 | 16 | text = globals.converter._dispatch('makehtml.hashCodeTags.after', text, options, globals).getText(); 17 | return text; 18 | }); 19 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/hashElement.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makehtml.hashElement', function (text, options, globals) { 2 | 'use strict'; 3 | 4 | return function (wholeMatch, m1) { 5 | var blockText = m1; 6 | 7 | // Undo double lines 8 | blockText = blockText.replace(/\n\n/g, '\n'); 9 | blockText = blockText.replace(/^\n/, ''); 10 | 11 | // strip trailing blank lines 12 | blockText = blockText.replace(/\n+$/g, ''); 13 | 14 | // Replace the element text with a marker ("¨KxK" where x is its key) 15 | blockText = '\n\n¨K' + (globals.gHtmlBlocks.push(blockText) - 1) + 'K\n\n'; 16 | 17 | return blockText; 18 | }; 19 | }); 20 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/hashHTMLBlocks.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makehtml.hashHTMLBlocks', function (text, options, globals) { 2 | 'use strict'; 3 | text = globals.converter._dispatch('makehtml.hashHTMLBlocks.before', text, options, globals).getText(); 4 | 5 | var blockTags = [ 6 | 'pre', 7 | 'div', 8 | 'h1', 9 | 'h2', 10 | 'h3', 11 | 'h4', 12 | 'h5', 13 | 'h6', 14 | 'blockquote', 15 | 'table', 16 | 'dl', 17 | 'ol', 18 | 'ul', 19 | 'script', 20 | 'noscript', 21 | 'form', 22 | 'fieldset', 23 | 'iframe', 24 | 'math', 25 | 'style', 26 | 'section', 27 | 'header', 28 | 'footer', 29 | 'nav', 30 | 'article', 31 | 'aside', 32 | 'address', 33 | 'audio', 34 | 'canvas', 35 | 'figure', 36 | 'hgroup', 37 | 'output', 38 | 'video', 39 | 'details', 40 | 'p' 41 | ], 42 | repFunc = function (wholeMatch, match, left, right) { 43 | var txt = wholeMatch; 44 | // check if this html element is marked as markdown 45 | // if so, it's contents should be parsed as markdown 46 | if (left.search(/\bmarkdown\b/) !== -1) { 47 | txt = left + globals.converter.makeHtml(match) + right; 48 | } 49 | return '\n\n¨K' + (globals.gHtmlBlocks.push(txt) - 1) + 'K\n\n'; 50 | }; 51 | 52 | if (options.backslashEscapesHTMLTags) { 53 | // encode backslash escaped HTML tags 54 | text = text.replace(/\\<(\/?[^>]+?)>/g, function (wm, inside) { 55 | return '<' + inside + '>'; 56 | }); 57 | } 58 | 59 | // hash HTML Blocks 60 | for (var i = 0; i < blockTags.length; ++i) { 61 | 62 | var opTagPos, 63 | rgx1 = new RegExp('^ {0,3}(<' + blockTags[i] + '\\b[^>]*>)', 'im'), 64 | patLeft = '<' + blockTags[i] + '\\b[^>]*>', 65 | patRight = ''; 66 | // 1. Look for the first position of the first opening HTML tag in the text 67 | while ((opTagPos = showdown.helper.regexIndexOf(text, rgx1)) !== -1) { 68 | 69 | // if the HTML tag is \ escaped, we need to escape it and break 70 | 71 | 72 | //2. Split the text in that position 73 | var subTexts = showdown.helper.splitAtIndex(text, opTagPos), 74 | //3. Match recursively 75 | newSubText1 = showdown.helper.replaceRecursiveRegExp(subTexts[1], repFunc, patLeft, patRight, 'im'); 76 | 77 | // prevent an infinite loop 78 | if (newSubText1 === subTexts[1]) { 79 | break; 80 | } 81 | text = subTexts[0].concat(newSubText1); 82 | } 83 | } 84 | // HR SPECIAL CASE 85 | text = text.replace(/(\n {0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g, 86 | showdown.subParser('makehtml.hashElement')(text, options, globals)); 87 | 88 | // Special case for standalone HTML comments 89 | text = showdown.helper.replaceRecursiveRegExp(text, function (txt) { 90 | return '\n\n¨K' + (globals.gHtmlBlocks.push(txt) - 1) + 'K\n\n'; 91 | }, '^ {0,3}', 'gm'); 92 | 93 | // PHP and ASP-style processor instructions ( and <%...%>) 94 | text = text.replace(/\n\n( {0,3}<([?%])[^\r]*?\2>[ \t]*(?=\n{2,}))/g, 95 | showdown.subParser('makehtml.hashElement')(text, options, globals)); 96 | 97 | text = globals.converter._dispatch('makehtml.hashHTMLBlocks.after', text, options, globals).getText(); 98 | return text; 99 | }); 100 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/hashHTMLSpans.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Hash span elements that should not be parsed as markdown 3 | */ 4 | showdown.subParser('makehtml.hashHTMLSpans', function (text, options, globals) { 5 | 'use strict'; 6 | text = globals.converter._dispatch('makehtml.hashHTMLSpans.before', text, options, globals).getText(); 7 | 8 | // Hash Self Closing tags 9 | text = text.replace(/<[^>]+?\/>/gi, function (wm) { 10 | return showdown.helper._hashHTMLSpan(wm, globals); 11 | }); 12 | 13 | // Hash tags without properties 14 | text = text.replace(/<([^>]+?)>[\s\S]*?<\/\1>/g, function (wm) { 15 | return showdown.helper._hashHTMLSpan(wm, globals); 16 | }); 17 | 18 | // Hash tags with properties 19 | text = text.replace(/<([^>]+?)\s[^>]+?>[\s\S]*?<\/\1>/g, function (wm) { 20 | return showdown.helper._hashHTMLSpan(wm, globals); 21 | }); 22 | 23 | // Hash self closing tags without /> 24 | text = text.replace(/<[^>]+?>/gi, function (wm) { 25 | return showdown.helper._hashHTMLSpan(wm, globals); 26 | }); 27 | 28 | text = globals.converter._dispatch('makehtml.hashHTMLSpans.after', text, options, globals).getText(); 29 | return text; 30 | }); 31 | 32 | /** 33 | * Unhash HTML spans 34 | */ 35 | showdown.subParser('makehtml.unhashHTMLSpans', function (text, options, globals) { 36 | 'use strict'; 37 | text = globals.converter._dispatch('makehtml.unhashHTMLSpans.before', text, options, globals).getText(); 38 | 39 | for (var i = 0; i < globals.gHtmlSpans.length; ++i) { 40 | var repText = globals.gHtmlSpans[i], 41 | // limiter to prevent infinite loop (assume 10 as limit for recurse) 42 | limit = 0; 43 | 44 | while (/¨C(\d+)C/.test(repText)) { 45 | var num = RegExp.$1; 46 | repText = repText.replace('¨C' + num + 'C', globals.gHtmlSpans[num]); 47 | if (limit === 10) { 48 | console.error('maximum nesting of 10 spans reached!!!'); 49 | break; 50 | } 51 | ++limit; 52 | } 53 | text = text.replace('¨C' + i + 'C', repText); 54 | } 55 | 56 | text = globals.converter._dispatch('makehtml.unhashHTMLSpans.after', text, options, globals).getText(); 57 | return text; 58 | }); 59 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/hashPreCodeTags.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Hash and escape
 elements that should not be parsed as markdown
 3 |  */
 4 | showdown.subParser('makehtml.hashPreCodeTags', function (text, options, globals) {
 5 |   'use strict';
 6 |   text = globals.converter._dispatch('makehtml.hashPreCodeTags.before', text, options, globals).getText();
 7 | 
 8 |   var repFunc = function (wholeMatch, match, left, right) {
 9 |     // encode html entities
10 |     var codeblock = left + showdown.subParser('makehtml.encodeCode')(match, options, globals) + right;
11 |     return '\n\n¨G' + (globals.ghCodeBlocks.push({text: wholeMatch, codeblock: codeblock}) - 1) + 'G\n\n';
12 |   };
13 | 
14 |   // Hash 

15 |   text = showdown.helper.replaceRecursiveRegExp(text, repFunc, '^ {0,3}]*>\\s*]*>', '^ {0,3}\\s*
', 'gim'); 16 | 17 | text = globals.converter._dispatch('makehtml.hashPreCodeTags.after', text, options, globals).getText(); 18 | return text; 19 | }); 20 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/headers.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makehtml.headers', function (text, options, globals) { 2 | 'use strict'; 3 | 4 | text = globals.converter._dispatch('makehtml.headers.before', text, options, globals).getText(); 5 | 6 | var headerLevelStart = (isNaN(parseInt(options.headerLevelStart))) ? 1 : parseInt(options.headerLevelStart), 7 | 8 | // Set text-style headers: 9 | // Header 1 10 | // ======== 11 | // 12 | // Header 2 13 | // -------- 14 | // 15 | setextRegexH1 = (options.smoothLivePreview) ? /^(.+)[ \t]*\n={2,}[ \t]*\n+/gm : /^(.+)[ \t]*\n=+[ \t]*\n+/gm, 16 | setextRegexH2 = (options.smoothLivePreview) ? /^(.+)[ \t]*\n-{2,}[ \t]*\n+/gm : /^(.+)[ \t]*\n-+[ \t]*\n+/gm; 17 | 18 | text = text.replace(setextRegexH1, function (wholeMatch, m1) { 19 | 20 | var spanGamut = showdown.subParser('makehtml.spanGamut')(m1, options, globals), 21 | hID = (options.noHeaderId) ? '' : ' id="' + headerId(m1) + '"', 22 | hLevel = headerLevelStart, 23 | hashBlock = '' + spanGamut + ''; 24 | return showdown.subParser('makehtml.hashBlock')(hashBlock, options, globals); 25 | }); 26 | 27 | text = text.replace(setextRegexH2, function (matchFound, m1) { 28 | var spanGamut = showdown.subParser('makehtml.spanGamut')(m1, options, globals), 29 | hID = (options.noHeaderId) ? '' : ' id="' + headerId(m1) + '"', 30 | hLevel = headerLevelStart + 1, 31 | hashBlock = '' + spanGamut + ''; 32 | return showdown.subParser('makehtml.hashBlock')(hashBlock, options, globals); 33 | }); 34 | 35 | // atx-style headers: 36 | // # Header 1 37 | // ## Header 2 38 | // ## Header 2 with closing hashes ## 39 | // ... 40 | // ###### Header 6 41 | // 42 | var atxStyle = (options.requireSpaceBeforeHeadingText) ? /^(#{1,6})[ \t]+(.+?)[ \t]*#*\n+/gm : /^(#{1,6})[ \t]*(.+?)[ \t]*#*\n+/gm; 43 | 44 | text = text.replace(atxStyle, function (wholeMatch, m1, m2) { 45 | var hText = m2; 46 | if (options.customizedHeaderId) { 47 | hText = m2.replace(/\s?{([^{]+?)}\s*$/, ''); 48 | } 49 | 50 | var span = showdown.subParser('makehtml.spanGamut')(hText, options, globals), 51 | hID = (options.noHeaderId) ? '' : ' id="' + headerId(m2) + '"', 52 | hLevel = headerLevelStart - 1 + m1.length, 53 | header = '' + span + ''; 54 | 55 | return showdown.subParser('makehtml.hashBlock')(header, options, globals); 56 | }); 57 | 58 | function headerId (m) { 59 | var title, 60 | prefix; 61 | 62 | // It is separate from other options to allow combining prefix and customized 63 | if (options.customizedHeaderId) { 64 | var match = m.match(/{([^{]+?)}\s*$/); 65 | if (match && match[1]) { 66 | m = match[1]; 67 | } 68 | } 69 | 70 | title = m; 71 | 72 | // Prefix id to prevent causing inadvertent pre-existing style matches. 73 | if (showdown.helper.isString(options.prefixHeaderId)) { 74 | prefix = options.prefixHeaderId; 75 | } else if (options.prefixHeaderId === true) { 76 | prefix = 'section-'; 77 | } else { 78 | prefix = ''; 79 | } 80 | 81 | if (!options.rawPrefixHeaderId) { 82 | title = prefix + title; 83 | } 84 | 85 | if (options.ghCompatibleHeaderId) { 86 | title = title 87 | .replace(/ /g, '-') 88 | // replace previously escaped chars (&, ¨ and $) 89 | .replace(/&/g, '') 90 | .replace(/¨T/g, '') 91 | .replace(/¨D/g, '') 92 | // replace rest of the chars (&~$ are repeated as they might have been escaped) 93 | // borrowed from github's redcarpet (some they should produce similar results) 94 | .replace(/[&+$,\/:;=?@"#{}|^¨~\[\]`\\*)(%.!'<>]/g, '') 95 | .toLowerCase(); 96 | } else if (options.rawHeaderId) { 97 | title = title 98 | .replace(/ /g, '-') 99 | // replace previously escaped chars (&, ¨ and $) 100 | .replace(/&/g, '&') 101 | .replace(/¨T/g, '¨') 102 | .replace(/¨D/g, '$') 103 | // replace " and ' 104 | .replace(/["']/g, '-') 105 | .toLowerCase(); 106 | } else { 107 | title = title 108 | .replace(/[^\w]/g, '') 109 | .toLowerCase(); 110 | } 111 | 112 | if (options.rawPrefixHeaderId) { 113 | title = prefix + title; 114 | } 115 | 116 | if (globals.hashLinkCounts[title]) { 117 | title = title + '-' + (globals.hashLinkCounts[title]++); 118 | } else { 119 | globals.hashLinkCounts[title] = 1; 120 | } 121 | return title; 122 | } 123 | 124 | text = globals.converter._dispatch('makehtml.headers.after', text, options, globals).getText(); 125 | return text; 126 | }); 127 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/horizontalRule.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Turn Markdown horizontal rule shortcuts into
tags. 3 | * 4 | * Any 3 or more unindented consecutive hyphens, asterisks or underscores with or without a space beetween them 5 | * in a single line is considered a horizontal rule 6 | */ 7 | showdown.subParser('makehtml.horizontalRule', function (text, options, globals) { 8 | 'use strict'; 9 | text = globals.converter._dispatch('makehtml.horizontalRule.before', text, options, globals).getText(); 10 | 11 | var key = showdown.subParser('makehtml.hashBlock')('
', options, globals); 12 | text = text.replace(/^ {0,2}( ?-){3,}[ \t]*$/gm, key); 13 | text = text.replace(/^ {0,2}( ?\*){3,}[ \t]*$/gm, key); 14 | text = text.replace(/^ {0,2}( ?_){3,}[ \t]*$/gm, key); 15 | 16 | text = globals.converter._dispatch('makehtml.horizontalRule.after', text, options, globals).getText(); 17 | return text; 18 | }); 19 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/images.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Turn Markdown image shortcuts into tags. 3 | */ 4 | showdown.subParser('makehtml.images', function (text, options, globals) { 5 | 'use strict'; 6 | 7 | text = globals.converter._dispatch('makehtml.images.before', text, options, globals).getText(); 8 | 9 | var inlineRegExp = /!\[([^\]]*?)][ \t]*()\([ \t]??(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(["'])([^"]*?)\6)?[ \t]?\)/g, 10 | crazyRegExp = /!\[([^\]]*?)][ \t]*()\([ \t]?<([^>]*)>(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(?:(["'])([^"]*?)\6))?[ \t]?\)/g, 11 | base64RegExp = /!\[([^\]]*?)][ \t]*()\([ \t]??(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(["'])([^"]*?)\6)?[ \t]?\)/g, 12 | referenceRegExp = /!\[([^\]]*?)] ?(?:\n *)?\[([\s\S]*?)]()()()()()/g, 13 | refShortcutRegExp = /!\[([^\[\]]+)]()()()()()/g; 14 | 15 | function writeImageTagBase64 (wholeMatch, altText, linkId, url, width, height, m5, title) { 16 | url = url.replace(/\s/g, ''); 17 | return writeImageTag (wholeMatch, altText, linkId, url, width, height, m5, title); 18 | } 19 | 20 | function writeImageTagBaseUrl (wholeMatch, altText, linkId, url, width, height, m5, title) { 21 | url = showdown.helper.applyBaseUrl(options.relativePathBaseUrl, url); 22 | 23 | return writeImageTag (wholeMatch, altText, linkId, url, width, height, m5, title); 24 | } 25 | 26 | function writeImageTag (wholeMatch, altText, linkId, url, width, height, m5, title) { 27 | 28 | var gUrls = globals.gUrls, 29 | gTitles = globals.gTitles, 30 | gDims = globals.gDimensions; 31 | 32 | linkId = linkId.toLowerCase(); 33 | 34 | if (!title) { 35 | title = ''; 36 | } 37 | // Special case for explicit empty url 38 | if (wholeMatch.search(/\(? ?(['"].*['"])?\)$/m) > -1) { 39 | url = ''; 40 | 41 | } else if (url === '' || url === null) { 42 | if (linkId === '' || linkId === null) { 43 | // lower-case and turn embedded newlines into spaces 44 | linkId = altText.toLowerCase().replace(/ ?\n/g, ' '); 45 | } 46 | url = '#' + linkId; 47 | 48 | if (!showdown.helper.isUndefined(gUrls[linkId])) { 49 | url = gUrls[linkId]; 50 | if (!showdown.helper.isUndefined(gTitles[linkId])) { 51 | title = gTitles[linkId]; 52 | } 53 | if (!showdown.helper.isUndefined(gDims[linkId])) { 54 | width = gDims[linkId].width; 55 | height = gDims[linkId].height; 56 | } 57 | } else { 58 | return wholeMatch; 59 | } 60 | } 61 | 62 | altText = altText 63 | .replace(/"/g, '"') 64 | //altText = showdown.helper.escapeCharacters(altText, '*_', false); 65 | .replace(showdown.helper.regexes.asteriskDashTildeAndColon, showdown.helper.escapeCharactersCallback); 66 | //url = showdown.helper.escapeCharacters(url, '*_', false); 67 | url = url.replace(showdown.helper.regexes.asteriskDashTildeAndColon, showdown.helper.escapeCharactersCallback); 68 | var result = '' + altText + 'x "optional title") 95 | 96 | // base64 encoded images 97 | text = text.replace(base64RegExp, writeImageTagBase64); 98 | 99 | // cases with crazy urls like ./image/cat1).png 100 | text = text.replace(crazyRegExp, writeImageTagBaseUrl); 101 | 102 | // normal cases 103 | text = text.replace(inlineRegExp, writeImageTagBaseUrl); 104 | 105 | // handle reference-style shortcuts: ![img text] 106 | text = text.replace(refShortcutRegExp, writeImageTag); 107 | 108 | text = globals.converter._dispatch('makehtml.images.after', text, options, globals).getText(); 109 | return text; 110 | }); 111 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/italicsAndBold.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makehtml.italicsAndBold', function (text, options, globals) { 2 | 'use strict'; 3 | 4 | text = globals.converter._dispatch('makehtml.italicsAndBold.before', text, options, globals).getText(); 5 | 6 | // it's faster to have 3 separate regexes for each case than have just one 7 | // because of backtracing, in some cases, it could lead to an exponential effect 8 | // called "catastrophic backtrace". Ominous! 9 | 10 | function parseInside (txt, left, right) { 11 | return left + txt + right; 12 | } 13 | 14 | // Parse underscores 15 | if (options.literalMidWordUnderscores) { 16 | text = text.replace(/\b___(\S[\s\S]*?)___\b/g, function (wm, txt) { 17 | return parseInside (txt, '', ''); 18 | }); 19 | text = text.replace(/\b__(\S[\s\S]*?)__\b/g, function (wm, txt) { 20 | return parseInside (txt, '', ''); 21 | }); 22 | text = text.replace(/\b_(\S[\s\S]*?)_\b/g, function (wm, txt) { 23 | return parseInside (txt, '', ''); 24 | }); 25 | } else { 26 | text = text.replace(/___(\S[\s\S]*?)___/g, function (wm, m) { 27 | return (/\S$/.test(m)) ? parseInside (m, '', '') : wm; 28 | }); 29 | text = text.replace(/__(\S[\s\S]*?)__/g, function (wm, m) { 30 | return (/\S$/.test(m)) ? parseInside (m, '', '') : wm; 31 | }); 32 | text = text.replace(/_([^\s_][\s\S]*?)_/g, function (wm, m) { 33 | // !/^_[^_]/.test(m) - test if it doesn't start with __ (since it seems redundant, we removed it) 34 | return (/\S$/.test(m)) ? parseInside (m, '', '') : wm; 35 | }); 36 | } 37 | 38 | // Now parse asterisks 39 | /* 40 | if (options.literalMidWordAsterisks) { 41 | text = text.replace(/([^*]|^)\B\*\*\*(\S[\s\S]+?)\*\*\*\B(?!\*)/g, function (wm, lead, txt) { 42 | return parseInside (txt, lead + '', ''); 43 | }); 44 | text = text.replace(/([^*]|^)\B\*\*(\S[\s\S]+?)\*\*\B(?!\*)/g, function (wm, lead, txt) { 45 | return parseInside (txt, lead + '', ''); 46 | }); 47 | text = text.replace(/([^*]|^)\B\*(\S[\s\S]+?)\*\B(?!\*)/g, function (wm, lead, txt) { 48 | return parseInside (txt, lead + '', ''); 49 | }); 50 | } else { 51 | */ 52 | text = text.replace(/\*\*\*(\S[\s\S]*?)\*\*\*/g, function (wm, m) { 53 | return (/\S$/.test(m)) ? parseInside (m, '', '') : wm; 54 | }); 55 | text = text.replace(/\*\*(\S[\s\S]*?)\*\*/g, function (wm, m) { 56 | return (/\S$/.test(m)) ? parseInside (m, '', '') : wm; 57 | }); 58 | text = text.replace(/\*([^\s*][\s\S]*?)\*/g, function (wm, m) { 59 | // !/^\*[^*]/.test(m) - test if it doesn't start with ** (since it seems redundant, we removed it) 60 | return (/\S$/.test(m)) ? parseInside (m, '', '') : wm; 61 | }); 62 | //} 63 | 64 | text = globals.converter._dispatch('makehtml.italicsAndBold.after', text, options, globals).getText(); 65 | return text; 66 | }); 67 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/lists.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Form HTML ordered (numbered) and unordered (bulleted) lists. 3 | */ 4 | showdown.subParser('makehtml.lists', function (text, options, globals) { 5 | 'use strict'; 6 | 7 | /** 8 | * Process the contents of a single ordered or unordered list, splitting it 9 | * into individual list items. 10 | * @param {string} listStr 11 | * @param {boolean} trimTrailing 12 | * @returns {string} 13 | */ 14 | function processListItems (listStr, trimTrailing) { 15 | // The $g_list_level global keeps track of when we're inside a list. 16 | // Each time we enter a list, we increment it; when we leave a list, 17 | // we decrement. If it's zero, we're not in a list anymore. 18 | // 19 | // We do this because when we're not inside a list, we want to treat 20 | // something like this: 21 | // 22 | // I recommend upgrading to version 23 | // 8. Oops, now this line is treated 24 | // as a sub-list. 25 | // 26 | // As a single paragraph, despite the fact that the second line starts 27 | // with a digit-period-space sequence. 28 | // 29 | // Whereas when we're inside a list (or sub-list), that line will be 30 | // treated as the start of a sub-list. What a kludge, huh? This is 31 | // an aspect of Markdown's syntax that's hard to parse perfectly 32 | // without resorting to mind-reading. Perhaps the solution is to 33 | // change the syntax rules such that sub-lists must start with a 34 | // starting cardinal number; e.g. "1." or "a.". 35 | globals.gListLevel++; 36 | 37 | // trim trailing blank lines: 38 | listStr = listStr.replace(/\n{2,}$/, '\n'); 39 | 40 | // attacklab: add sentinel to emulate \z 41 | listStr += '¨0'; 42 | 43 | var rgx = /(\n)?(^ {0,3})([*+-]|\d+[.])[ \t]+((\[([xX ])])?[ \t]*[^\r]+?(\n{1,2}))(?=\n*(¨0| {0,3}([*+-]|\d+[.])[ \t]+))/gm, 44 | isParagraphed = (/\n[ \t]*\n(?!¨0)/.test(listStr)); 45 | 46 | // Since version 1.5, nesting sublists requires 4 spaces (or 1 tab) indentation, 47 | // which is a syntax breaking change 48 | // activating this option reverts to old behavior 49 | // This will be removed in version 2.0 50 | if (options.disableForced4SpacesIndentedSublists) { 51 | rgx = /(\n)?(^ {0,3})([*+-]|\d+[.])[ \t]+((\[([xX ])])?[ \t]*[^\r]+?(\n{1,2}))(?=\n*(¨0|\2([*+-]|\d+[.])[ \t]+))/gm; 52 | } 53 | 54 | listStr = listStr.replace(rgx, function (wholeMatch, m1, m2, m3, m4, taskbtn, checked) { 55 | checked = (checked && checked.trim() !== ''); 56 | 57 | var item = showdown.subParser('makehtml.outdent')(m4, options, globals), 58 | bulletStyle = ''; 59 | 60 | // Support for github tasklists 61 | if (taskbtn && options.tasklists) { 62 | 63 | // Style used for tasklist bullets 64 | bulletStyle = ' class="task-list-item'; 65 | if (options.moreStyling) {bulletStyle += checked ? ' task-list-item-complete' : '';} 66 | bulletStyle += '" style="list-style-type: none;"'; 67 | 68 | item = item.replace(/^[ \t]*\[([xX ])?]/m, function () { 69 | var otp = '
  • a
  • 82 | // instead of: 83 | //
    • - - a
    84 | // So, to prevent it, we will put a marker (¨A)in the beginning of the line 85 | // Kind of hackish/monkey patching, but seems more effective than overcomplicating the list parser 86 | item = item.replace(/^([-*+]|\d\.)[ \t]+[\S\n ]*/g, function (wm2) { 87 | return '¨A' + wm2; 88 | }); 89 | 90 | // SPECIAL CASE: a heading followed by a paragraph of text that is not separated by a double newline 91 | // or/nor indented. ex: 92 | // 93 | // - # foo 94 | // bar is great 95 | // 96 | // While this does now follow the spec per se, not allowing for this might cause confusion since 97 | // header blocks don't need double-newlines after 98 | if (/^#+.+\n.+/.test(item)) { 99 | item = item.replace(/^(#+.+)$/m, '$1\n'); 100 | } 101 | 102 | // m1 - Leading line or 103 | // Has a double return (multi paragraph) 104 | if (m1 || (item.search(/\n{2,}/) > -1)) { 105 | item = showdown.subParser('makehtml.githubCodeBlocks')(item, options, globals); 106 | item = showdown.subParser('makehtml.blockQuotes')(item, options, globals); 107 | item = showdown.subParser('makehtml.headers')(item, options, globals); 108 | item = showdown.subParser('makehtml.lists')(item, options, globals); 109 | item = showdown.subParser('makehtml.codeBlocks')(item, options, globals); 110 | item = showdown.subParser('makehtml.tables')(item, options, globals); 111 | item = showdown.subParser('makehtml.hashHTMLBlocks')(item, options, globals); 112 | //item = showdown.subParser('makehtml.paragraphs')(item, options, globals); 113 | 114 | // TODO: This is a copy of the paragraph parser 115 | // This is a provisory fix for issue #494 116 | // For a permanente fix we need to rewrite the paragraph parser, passing the unhashify logic outside 117 | // so that we can call the paragraph parser without accidently unashifying previously parsed blocks 118 | 119 | // Strip leading and trailing lines: 120 | item = item.replace(/^\n+/g, ''); 121 | item = item.replace(/\n+$/g, ''); 122 | 123 | var grafs = item.split(/\n{2,}/g), 124 | grafsOut = [], 125 | end = grafs.length; // Wrap

    tags 126 | 127 | for (var i = 0; i < end; i++) { 128 | var str = grafs[i]; 129 | // if this is an HTML marker, copy it 130 | if (str.search(/¨([KG])(\d+)\1/g) >= 0) { 131 | grafsOut.push(str); 132 | 133 | // test for presence of characters to prevent empty lines being parsed 134 | // as paragraphs (resulting in undesired extra empty paragraphs) 135 | } else if (str.search(/\S/) >= 0) { 136 | str = showdown.subParser('makehtml.spanGamut')(str, options, globals); 137 | str = str.replace(/^([ \t]*)/g, '

    '); 138 | str += '

    '; 139 | grafsOut.push(str); 140 | } 141 | } 142 | item = grafsOut.join('\n'); 143 | // Strip leading and trailing lines: 144 | item = item.replace(/^\n+/g, ''); 145 | item = item.replace(/\n+$/g, ''); 146 | 147 | } else { 148 | 149 | // Recursion for sub-lists: 150 | item = showdown.subParser('makehtml.lists')(item, options, globals); 151 | item = item.replace(/\n$/, ''); // chomp(item) 152 | item = showdown.subParser('makehtml.hashHTMLBlocks')(item, options, globals); 153 | 154 | // Colapse double linebreaks 155 | item = item.replace(/\n\n+/g, '\n\n'); 156 | 157 | if (isParagraphed) { 158 | item = showdown.subParser('makehtml.paragraphs')(item, options, globals); 159 | } else { 160 | item = showdown.subParser('makehtml.spanGamut')(item, options, globals); 161 | } 162 | } 163 | 164 | // now we need to remove the marker (¨A) 165 | item = item.replace('¨A', ''); 166 | // we can finally wrap the line in list item tags 167 | item = '' + item + '\n'; 168 | 169 | return item; 170 | }); 171 | 172 | // attacklab: strip sentinel 173 | listStr = listStr.replace(/¨0/g, ''); 174 | 175 | globals.gListLevel--; 176 | 177 | if (trimTrailing) { 178 | listStr = listStr.replace(/\s+$/, ''); 179 | } 180 | 181 | return listStr; 182 | } 183 | 184 | function styleStartNumber (list, listType) { 185 | // check if ol and starts by a number different than 1 186 | if (listType === 'ol') { 187 | var res = list.match(/^ *(\d+)\./); 188 | if (res && res[1] !== '1') { 189 | return ' start="' + res[1] + '"'; 190 | } 191 | } 192 | return ''; 193 | } 194 | 195 | /** 196 | * Check and parse consecutive lists (better fix for issue #142) 197 | * @param {string} list 198 | * @param {string} listType 199 | * @param {boolean} trimTrailing 200 | * @returns {string} 201 | */ 202 | function parseConsecutiveLists (list, listType, trimTrailing) { 203 | // check if we caught 2 or more consecutive lists by mistake 204 | // we use the counterRgx, meaning if listType is UL we look for OL and vice versa 205 | var olRgx = (options.disableForced4SpacesIndentedSublists) ? /^ ?\d+\.[ \t]/gm : /^ {0,3}\d+\.[ \t]/gm, 206 | ulRgx = (options.disableForced4SpacesIndentedSublists) ? /^ ?[*+-][ \t]/gm : /^ {0,3}[*+-][ \t]/gm, 207 | counterRxg = (listType === 'ul') ? olRgx : ulRgx, 208 | result = ''; 209 | 210 | if (list.search(counterRxg) !== -1) { 211 | (function parseCL (txt) { 212 | var pos = txt.search(counterRxg), 213 | style = styleStartNumber(list, listType); 214 | if (pos !== -1) { 215 | // slice 216 | result += '\n\n<' + listType + style + '>\n' + processListItems(txt.slice(0, pos), !!trimTrailing) + '\n'; 217 | 218 | // invert counterType and listType 219 | listType = (listType === 'ul') ? 'ol' : 'ul'; 220 | counterRxg = (listType === 'ul') ? olRgx : ulRgx; 221 | 222 | //recurse 223 | parseCL(txt.slice(pos)); 224 | } else { 225 | result += '\n\n<' + listType + style + '>\n' + processListItems(txt, !!trimTrailing) + '\n'; 226 | } 227 | })(list); 228 | } else { 229 | var style = styleStartNumber(list, listType); 230 | result = '\n\n<' + listType + style + '>\n' + processListItems(list, !!trimTrailing) + '\n'; 231 | } 232 | 233 | return result; 234 | } 235 | 236 | // Start of list parsing 237 | var subListRgx = /^(( {0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(¨0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm; 238 | var mainListRgx = /(\n\n|^\n?)(( {0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(¨0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm; 239 | 240 | text = globals.converter._dispatch('lists.before', text, options, globals).getText(); 241 | // add sentinel to hack around khtml/safari bug: 242 | // http://bugs.webkit.org/show_bug.cgi?id=11231 243 | text += '¨0'; 244 | 245 | if (globals.gListLevel) { 246 | text = text.replace(subListRgx, function (wholeMatch, list, m2) { 247 | var listType = (m2.search(/[*+-]/g) > -1) ? 'ul' : 'ol'; 248 | return parseConsecutiveLists(list, listType, true); 249 | }); 250 | } else { 251 | text = text.replace(mainListRgx, function (wholeMatch, m1, list, m3) { 252 | var listType = (m3.search(/[*+-]/g) > -1) ? 'ul' : 'ol'; 253 | return parseConsecutiveLists(list, listType, false); 254 | }); 255 | } 256 | 257 | // strip sentinel 258 | text = text.replace(/¨0/, ''); 259 | text = globals.converter._dispatch('makehtml.lists.after', text, options, globals).getText(); 260 | return text; 261 | }); 262 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/metadata.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Parse metadata at the top of the document 3 | */ 4 | showdown.subParser('makehtml.metadata', function (text, options, globals) { 5 | 'use strict'; 6 | 7 | if (!options.metadata) { 8 | return text; 9 | } 10 | 11 | text = globals.converter._dispatch('makehtml.metadata.before', text, options, globals).getText(); 12 | 13 | function parseMetadataContents (content) { 14 | // raw is raw so it's not changed in any way 15 | globals.metadata.raw = content; 16 | 17 | // escape chars forbidden in html attributes 18 | // double quotes 19 | content = content 20 | // ampersand first 21 | .replace(/&/g, '&') 22 | // double quotes 23 | .replace(/"/g, '"'); 24 | 25 | // Restore dollar signs and tremas 26 | content = content 27 | .replace(/¨D/g, '$$') 28 | .replace(/¨T/g, '¨'); 29 | 30 | content = content.replace(/\n {4}/g, ' '); 31 | content.replace(/^([\S ]+): +([\s\S]+?)$/gm, function (wm, key, value) { 32 | globals.metadata.parsed[key] = value; 33 | return ''; 34 | }); 35 | } 36 | 37 | text = text.replace(/^\s*«««+\s*(\S*?)\n([\s\S]+?)\n»»»+\s*\n/, function (wholematch, format, content) { 38 | parseMetadataContents(content); 39 | return '¨M'; 40 | }); 41 | 42 | text = text.replace(/^\s*---+\s*(\S*?)\n([\s\S]+?)\n---+\s*\n/, function (wholematch, format, content) { 43 | if (format) { 44 | globals.metadata.format = format; 45 | } 46 | parseMetadataContents(content); 47 | return '¨M'; 48 | }); 49 | 50 | text = text.replace(/¨M/g, ''); 51 | 52 | text = globals.converter._dispatch('makehtml.metadata.after', text, options, globals).getText(); 53 | return text; 54 | }); 55 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/outdent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Remove one level of line-leading tabs or spaces 3 | */ 4 | showdown.subParser('makehtml.outdent', function (text, options, globals) { 5 | 'use strict'; 6 | text = globals.converter._dispatch('makehtml.outdent.before', text, options, globals).getText(); 7 | 8 | // attacklab: hack around Konqueror 3.5.4 bug: 9 | // "----------bug".replace(/^-/g,"") == "bug" 10 | text = text.replace(/^(\t|[ ]{1,4})/gm, '¨0'); // attacklab: g_tab_width 11 | 12 | // attacklab: clean up hack 13 | text = text.replace(/¨0/g, ''); 14 | 15 | text = globals.converter._dispatch('makehtml.outdent.after', text, options, globals).getText(); 16 | return text; 17 | }); 18 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/paragraphs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | showdown.subParser('makehtml.paragraphs', function (text, options, globals) { 5 | 'use strict'; 6 | 7 | text = globals.converter._dispatch('makehtml.paragraphs.before', text, options, globals).getText(); 8 | // Strip leading and trailing lines: 9 | text = text.replace(/^\n+/g, ''); 10 | text = text.replace(/\n+$/g, ''); 11 | 12 | var grafs = text.split(/\n{2,}/g), 13 | grafsOut = [], 14 | end = grafs.length; // Wrap

    tags 15 | 16 | for (var i = 0; i < end; i++) { 17 | var str = grafs[i]; 18 | // if this is an HTML marker, copy it 19 | if (str.search(/¨(K|G)(\d+)\1/g) >= 0) { 20 | grafsOut.push(str); 21 | 22 | // test for presence of characters to prevent empty lines being parsed 23 | // as paragraphs (resulting in undesired extra empty paragraphs) 24 | } else if (str.search(/\S/) >= 0) { 25 | str = showdown.subParser('makehtml.spanGamut')(str, options, globals); 26 | str = str.replace(/^([ \t]*)/g, '

    '); 27 | str += '

    '; 28 | grafsOut.push(str); 29 | } 30 | } 31 | 32 | /** Unhashify HTML blocks */ 33 | end = grafsOut.length; 34 | for (i = 0; i < end; i++) { 35 | var blockText = '', 36 | grafsOutIt = grafsOut[i], 37 | codeFlag = false; 38 | // if this is a marker for an html block... 39 | // use RegExp.test instead of string.search because of QML bug 40 | while (/¨(K|G)(\d+)\1/.test(grafsOutIt)) { 41 | var delim = RegExp.$1, 42 | num = RegExp.$2; 43 | 44 | if (delim === 'K') { 45 | blockText = globals.gHtmlBlocks[num]; 46 | } else { 47 | // we need to check if ghBlock is a false positive 48 | if (codeFlag) { 49 | // use encoded version of all text 50 | blockText = showdown.subParser('makehtml.encodeCode')(globals.ghCodeBlocks[num].text, options, globals); 51 | } else { 52 | blockText = globals.ghCodeBlocks[num].codeblock; 53 | } 54 | } 55 | blockText = blockText.replace(/\$/g, '$$$$'); // Escape any dollar signs 56 | 57 | grafsOutIt = grafsOutIt.replace(/(\n\n)?¨(K|G)\d+\2(\n\n)?/, blockText); 58 | // Check if grafsOutIt is a pre->code 59 | if (/^]*>\s*]*>/.test(grafsOutIt)) { 60 | codeFlag = true; 61 | } 62 | } 63 | grafsOut[i] = grafsOutIt; 64 | } 65 | text = grafsOut.join('\n'); 66 | // Strip leading and trailing lines: 67 | text = text.replace(/^\n+/g, ''); 68 | text = text.replace(/\n+$/g, ''); 69 | return globals.converter._dispatch('makehtml.paragraphs.after', text, options, globals).getText(); 70 | }); 71 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/runExtension.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Run extension 3 | */ 4 | showdown.subParser('makehtml.runExtension', function (ext, text, options, globals) { 5 | 'use strict'; 6 | 7 | if (ext.filter) { 8 | text = ext.filter(text, globals.converter, options); 9 | 10 | } else if (ext.regex) { 11 | // TODO remove this when old extension loading mechanism is deprecated 12 | var re = ext.regex; 13 | if (!(re instanceof RegExp)) { 14 | re = new RegExp(re, 'g'); 15 | } 16 | text = text.replace(re, ext.replace); 17 | } 18 | 19 | return text; 20 | }); 21 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/spanGamut.js: -------------------------------------------------------------------------------- 1 | /** 2 | * These are all the transformations that occur *within* block-level 3 | * tags like paragraphs, headers, and list items. 4 | */ 5 | showdown.subParser('makehtml.spanGamut', function (text, options, globals) { 6 | 'use strict'; 7 | 8 | text = globals.converter._dispatch('makehtml.span.before', text, options, globals).getText(); 9 | 10 | text = showdown.subParser('makehtml.codeSpans')(text, options, globals); 11 | text = showdown.subParser('makehtml.escapeSpecialCharsWithinTagAttributes')(text, options, globals); 12 | text = showdown.subParser('makehtml.encodeBackslashEscapes')(text, options, globals); 13 | 14 | // Process link and image tags. Images must come first, 15 | // because ![foo][f] looks like a link. 16 | text = showdown.subParser('makehtml.images')(text, options, globals); 17 | 18 | text = globals.converter._dispatch('smakehtml.links.before', text, options, globals).getText(); 19 | text = showdown.subParser('makehtml.links')(text, options, globals); 20 | text = globals.converter._dispatch('smakehtml.links.after', text, options, globals).getText(); 21 | 22 | //text = showdown.subParser('makehtml.autoLinks')(text, options, globals); 23 | //text = showdown.subParser('makehtml.simplifiedAutoLinks')(text, options, globals); 24 | text = showdown.subParser('makehtml.emoji')(text, options, globals); 25 | text = showdown.subParser('makehtml.underline')(text, options, globals); 26 | text = showdown.subParser('makehtml.italicsAndBold')(text, options, globals); 27 | text = showdown.subParser('makehtml.strikethrough')(text, options, globals); 28 | text = showdown.subParser('makehtml.ellipsis')(text, options, globals); 29 | 30 | // we need to hash HTML tags inside spans 31 | text = showdown.subParser('makehtml.hashHTMLSpans')(text, options, globals); 32 | 33 | // now we encode amps and angles 34 | text = showdown.subParser('makehtml.encodeAmpsAndAngles')(text, options, globals); 35 | 36 | // Do hard breaks 37 | if (options.simpleLineBreaks) { 38 | // GFM style hard breaks 39 | // only add line breaks if the text does not contain a block (special case for lists) 40 | if (!/\n\n¨K/.test(text)) { 41 | text = text.replace(/\n+/g, '
    \n'); 42 | } 43 | } else { 44 | // Vanilla hard breaks 45 | text = text.replace(/ +\n/g, '
    \n'); 46 | } 47 | 48 | text = globals.converter._dispatch('makehtml.spanGamut.after', text, options, globals).getText(); 49 | return text; 50 | }); 51 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/strikethrough.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makehtml.strikethrough', function (text, options, globals) { 2 | 'use strict'; 3 | 4 | if (options.strikethrough) { 5 | text = globals.converter._dispatch('makehtml.strikethrough.before', text, options, globals).getText(); 6 | text = text.replace(/(?:~){2}([\s\S]+?)(?:~){2}/g, function (wm, txt) { return '' + txt + ''; }); 7 | text = globals.converter._dispatch('makehtml.strikethrough.after', text, options, globals).getText(); 8 | } 9 | 10 | return text; 11 | }); 12 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/stripLinkDefinitions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Strips link definitions from text, stores the URLs and titles in 3 | * hash references. 4 | * Link defs are in the form: ^[id]: url "optional title" 5 | */ 6 | showdown.subParser('makehtml.stripLinkDefinitions', function (text, options, globals) { 7 | 'use strict'; 8 | 9 | var regex = /^ {0,3}\[([^\]]+)]:[ \t]*\n?[ \t]*\s]+)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*\n?[ \t]*(?:(\n*)["|'(](.+?)["|')][ \t]*)?(?:\n+|(?=¨0))/gm, 10 | base64Regex = /^ {0,3}\[([^\]]+)]:[ \t]*\n?[ \t]*?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*\n?[ \t]*(?:(\n*)["|'(](.+?)["|')][ \t]*)?(?:\n\n|(?=¨0)|(?=\n\[))/gm; 11 | 12 | // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug 13 | text += '¨0'; 14 | 15 | var replaceFunc = function (wholeMatch, linkId, url, width, height, blankLines, title) { 16 | 17 | // if there aren't two instances of linkId it must not be a reference link so back out 18 | linkId = linkId.toLowerCase(); 19 | if (text.toLowerCase().split(linkId).length - 1 < 2) { 20 | return wholeMatch; 21 | } 22 | if (url.match(/^data:.+?\/.+?;base64,/)) { 23 | // remove newlines 24 | globals.gUrls[linkId] = url.replace(/\s/g, ''); 25 | } else { 26 | url = showdown.helper.applyBaseUrl(options.relativePathBaseUrl, url); 27 | 28 | globals.gUrls[linkId] = showdown.subParser('makehtml.encodeAmpsAndAngles')(url, options, globals); // Link IDs are case-insensitive 29 | } 30 | 31 | if (blankLines) { 32 | // Oops, found blank lines, so it's not a title. 33 | // Put back the parenthetical statement we stole. 34 | return blankLines + title; 35 | 36 | } else { 37 | if (title) { 38 | globals.gTitles[linkId] = title.replace(/"|'/g, '"'); 39 | } 40 | if (options.parseImgDimensions && width && height) { 41 | globals.gDimensions[linkId] = { 42 | width: width, 43 | height: height 44 | }; 45 | } 46 | } 47 | // Completely remove the definition from the text 48 | return ''; 49 | }; 50 | 51 | // first we try to find base64 link references 52 | text = text.replace(base64Regex, replaceFunc); 53 | 54 | text = text.replace(regex, replaceFunc); 55 | 56 | // attacklab: strip sentinel 57 | text = text.replace(/¨0/, ''); 58 | 59 | return text; 60 | }); 61 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/tables.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makehtml.tables', function (text, options, globals) { 2 | 'use strict'; 3 | 4 | if (!options.tables) { 5 | return text; 6 | } 7 | 8 | var tableRgx = /^ {0,3}\|?.+\|.+\n {0,3}\|?[ \t]*:?[ \t]*[-=]{2,}[ \t]*:?[ \t]*\|[ \t]*:?[ \t]*[-=]{2,}[\s\S]+?(?:\n\n|¨0)/gm, 9 | //singeColTblRgx = /^ {0,3}\|.+\|\n {0,3}\|[ \t]*:?[ \t]*(?:[-=]){2,}[ \t]*:?[ \t]*\|[ \t]*\n(?: {0,3}\|.+\|\n)+(?:\n\n|¨0)/gm; 10 | singeColTblRgx = /^ {0,3}\|.+\|[ \t]*\n {0,3}\|[ \t]*:?[ \t]*[-=]{2,}[ \t]*:?[ \t]*\|[ \t]*\n( {0,3}\|.+\|[ \t]*\n)*(?:\n|¨0)/gm; 11 | 12 | function parseStyles (sLine) { 13 | if (/^:[ \t]*--*$/.test(sLine)) { 14 | return ' style="text-align:left;"'; 15 | } else if (/^--*[ \t]*:[ \t]*$/.test(sLine)) { 16 | return ' style="text-align:right;"'; 17 | } else if (/^:[ \t]*--*[ \t]*:$/.test(sLine)) { 18 | return ' style="text-align:center;"'; 19 | } else { 20 | return ''; 21 | } 22 | } 23 | 24 | function parseHeaders (header, style) { 25 | var id = ''; 26 | header = header.trim(); 27 | // support both tablesHeaderId and tableHeaderId due to error in documentation so we don't break backwards compatibility 28 | if (options.tablesHeaderId || options.tableHeaderId) { 29 | id = ' id="' + header.replace(/ /g, '_').toLowerCase() + '"'; 30 | } 31 | header = showdown.subParser('makehtml.spanGamut')(header, options, globals); 32 | 33 | return '' + header + '\n'; 34 | } 35 | 36 | function parseCells (cell, style) { 37 | var subText = showdown.subParser('makehtml.spanGamut')(cell, options, globals); 38 | return '' + subText + '\n'; 39 | } 40 | 41 | function buildTable (headers, cells) { 42 | var tb = '\n\n\n', 43 | tblLgn = headers.length; 44 | 45 | for (var i = 0; i < tblLgn; ++i) { 46 | tb += headers[i]; 47 | } 48 | tb += '\n\n\n'; 49 | 50 | for (i = 0; i < cells.length; ++i) { 51 | tb += '\n'; 52 | for (var ii = 0; ii < tblLgn; ++ii) { 53 | tb += cells[i][ii]; 54 | } 55 | tb += '\n'; 56 | } 57 | tb += '\n
    \n'; 58 | return tb; 59 | } 60 | 61 | function parseTable (rawTable) { 62 | var i, tableLines = rawTable.split('\n'); 63 | 64 | for (i = 0; i < tableLines.length; ++i) { 65 | // strip wrong first and last column if wrapped tables are used 66 | if (/^ {0,3}\|/.test(tableLines[i])) { 67 | tableLines[i] = tableLines[i].replace(/^ {0,3}\|/, ''); 68 | } 69 | if (/\|[ \t]*$/.test(tableLines[i])) { 70 | tableLines[i] = tableLines[i].replace(/\|[ \t]*$/, ''); 71 | } 72 | // parse code spans first, but we only support one line code spans 73 | 74 | tableLines[i] = showdown.subParser('makehtml.codeSpans')(tableLines[i], options, globals); 75 | } 76 | 77 | var rawHeaders = tableLines[0].split('|').map(function (s) { return s.trim();}), 78 | rawStyles = tableLines[1].split('|').map(function (s) { return s.trim();}), 79 | rawCells = [], 80 | headers = [], 81 | styles = [], 82 | cells = []; 83 | 84 | tableLines.shift(); 85 | tableLines.shift(); 86 | 87 | for (i = 0; i < tableLines.length; ++i) { 88 | if (tableLines[i].trim() === '') { 89 | continue; 90 | } 91 | rawCells.push( 92 | tableLines[i] 93 | .split('|') 94 | .map(function (s) { 95 | return s.trim(); 96 | }) 97 | ); 98 | } 99 | 100 | if (rawHeaders.length < rawStyles.length) { 101 | return rawTable; 102 | } 103 | 104 | for (i = 0; i < rawStyles.length; ++i) { 105 | styles.push(parseStyles(rawStyles[i])); 106 | } 107 | 108 | for (i = 0; i < rawHeaders.length; ++i) { 109 | if (showdown.helper.isUndefined(styles[i])) { 110 | styles[i] = ''; 111 | } 112 | headers.push(parseHeaders(rawHeaders[i], styles[i])); 113 | } 114 | 115 | for (i = 0; i < rawCells.length; ++i) { 116 | var row = []; 117 | for (var ii = 0; ii < headers.length; ++ii) { 118 | if (showdown.helper.isUndefined(rawCells[i][ii])) { 119 | 120 | } 121 | row.push(parseCells(rawCells[i][ii], styles[ii])); 122 | } 123 | cells.push(row); 124 | } 125 | 126 | return buildTable(headers, cells); 127 | } 128 | 129 | text = globals.converter._dispatch('makehtml.tables.before', text, options, globals).getText(); 130 | 131 | // find escaped pipe characters 132 | text = text.replace(/\\(\|)/g, showdown.helper.escapeCharactersCallback); 133 | 134 | // parse multi column tables 135 | text = text.replace(tableRgx, parseTable); 136 | 137 | // parse one column tables 138 | text = text.replace(singeColTblRgx, parseTable); 139 | 140 | text = globals.converter._dispatch('makehtml.tables.after', text, options, globals).getText(); 141 | 142 | return text; 143 | }); 144 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/underline.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makehtml.underline', function (text, options, globals) { 2 | 'use strict'; 3 | 4 | if (!options.underline) { 5 | return text; 6 | } 7 | 8 | text = globals.converter._dispatch('makehtml.underline.before', text, options, globals).getText(); 9 | 10 | if (options.literalMidWordUnderscores) { 11 | text = text.replace(/\b___(\S[\s\S]*?)___\b/g, function (wm, txt) { 12 | return '' + txt + ''; 13 | }); 14 | text = text.replace(/\b__(\S[\s\S]*?)__\b/g, function (wm, txt) { 15 | return '' + txt + ''; 16 | }); 17 | } else { 18 | text = text.replace(/___(\S[\s\S]*?)___/g, function (wm, m) { 19 | return (/\S$/.test(m)) ? '' + m + '' : wm; 20 | }); 21 | text = text.replace(/__(\S[\s\S]*?)__/g, function (wm, m) { 22 | return (/\S$/.test(m)) ? '' + m + '' : wm; 23 | }); 24 | } 25 | 26 | // escape remaining underscores to prevent them being parsed by italic and bold 27 | text = text.replace(/(_)/g, showdown.helper.escapeCharactersCallback); 28 | 29 | text = globals.converter._dispatch('makehtml.underline.after', text, options, globals).getText(); 30 | 31 | return text; 32 | }); 33 | -------------------------------------------------------------------------------- /src/subParsers/makehtml/unescapeSpecialChars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Swap back in all the special characters we've hidden. 3 | */ 4 | showdown.subParser('makehtml.unescapeSpecialChars', function (text, options, globals) { 5 | 'use strict'; 6 | text = globals.converter._dispatch('makehtml.unescapeSpecialChars.before', text, options, globals).getText(); 7 | 8 | text = text.replace(/¨E(\d+)E/g, function (wholeMatch, m1) { 9 | var charCodeToReplace = parseInt(m1); 10 | return String.fromCharCode(charCodeToReplace); 11 | }); 12 | 13 | text = globals.converter._dispatch('makehtml.unescapeSpecialChars.after', text, options, globals).getText(); 14 | return text; 15 | }); 16 | -------------------------------------------------------------------------------- /src/subParsers/makemarkdown/blockquote.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makeMarkdown.blockquote', function (node, options, globals) { 2 | 'use strict'; 3 | 4 | var txt = ''; 5 | if (node.hasChildNodes()) { 6 | var children = node.childNodes, 7 | childrenLength = children.length; 8 | 9 | for (var i = 0; i < childrenLength; ++i) { 10 | var innerTxt = showdown.subParser('makeMarkdown.node')(children[i], options, globals); 11 | 12 | if (innerTxt === '') { 13 | continue; 14 | } 15 | txt += innerTxt; 16 | } 17 | } 18 | // cleanup 19 | txt = txt.trim(); 20 | txt = '> ' + txt.split('\n').join('\n> '); 21 | return txt; 22 | }); 23 | -------------------------------------------------------------------------------- /src/subParsers/makemarkdown/break.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makeMarkdown.break', function () { 2 | 'use strict'; 3 | 4 | return ' \n'; 5 | }); 6 | -------------------------------------------------------------------------------- /src/subParsers/makemarkdown/codeBlock.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makeMarkdown.codeBlock', function (node, options, globals) { 2 | 'use strict'; 3 | 4 | var lang = node.getAttribute('language'), 5 | num = node.getAttribute('precodenum'); 6 | return '```' + lang + '\n' + globals.preList[num] + '\n```'; 7 | }); 8 | -------------------------------------------------------------------------------- /src/subParsers/makemarkdown/codeSpan.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makeMarkdown.codeSpan', function (node) { 2 | 'use strict'; 3 | 4 | return '`' + node.innerHTML + '`'; 5 | }); 6 | -------------------------------------------------------------------------------- /src/subParsers/makemarkdown/emphasis.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makeMarkdown.emphasis', function (node, options, globals) { 2 | 'use strict'; 3 | 4 | var txt = ''; 5 | if (node.hasChildNodes()) { 6 | txt += '*'; 7 | var children = node.childNodes, 8 | childrenLength = children.length; 9 | for (var i = 0; i < childrenLength; ++i) { 10 | txt += showdown.subParser('makeMarkdown.node')(children[i], options, globals); 11 | } 12 | txt += '*'; 13 | } 14 | return txt; 15 | }); 16 | -------------------------------------------------------------------------------- /src/subParsers/makemarkdown/header.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makeMarkdown.header', function (node, options, globals, headerLevel) { 2 | 'use strict'; 3 | 4 | var headerMark = new Array(headerLevel + 1).join('#'), 5 | txt = ''; 6 | 7 | if (node.hasChildNodes()) { 8 | txt = headerMark + ' '; 9 | var children = node.childNodes, 10 | childrenLength = children.length; 11 | 12 | for (var i = 0; i < childrenLength; ++i) { 13 | txt += showdown.subParser('makeMarkdown.node')(children[i], options, globals); 14 | } 15 | } 16 | return txt; 17 | }); 18 | -------------------------------------------------------------------------------- /src/subParsers/makemarkdown/hr.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makeMarkdown.hr', function () { 2 | 'use strict'; 3 | 4 | return '---'; 5 | }); 6 | -------------------------------------------------------------------------------- /src/subParsers/makemarkdown/image.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makeMarkdown.image', function (node) { 2 | 'use strict'; 3 | 4 | var txt = ''; 5 | if (node.hasAttribute('src') && node.getAttribute('src') !== '') { 6 | txt += '![' + node.getAttribute('alt') + ']('; 7 | txt += '<' + node.getAttribute('src') + '>'; 8 | if (node.hasAttribute('width') && node.hasAttribute('height')) { 9 | var width = node.getAttribute('width'); 10 | var height = node.getAttribute('height'); 11 | txt += ' =' + (width === 'auto' ? '*' : width) + 'x' + (height === 'auto' ? '*' : height); 12 | } 13 | 14 | if (node.hasAttribute('title')) { 15 | txt += ' "' + node.getAttribute('title') + '"'; 16 | } 17 | txt += ')'; 18 | } 19 | return txt; 20 | }); 21 | -------------------------------------------------------------------------------- /src/subParsers/makemarkdown/input.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makeMarkdown.input', function (node, options, globals) { 2 | 'use strict'; 3 | 4 | var txt = ''; 5 | if (node.getAttribute('checked') !== null) { 6 | txt += '[x]'; 7 | } else { 8 | txt += '[ ]'; 9 | } 10 | var children = node.childNodes, 11 | childrenLength = children.length; 12 | for (var i = 0; i < childrenLength; ++i) { 13 | txt += showdown.subParser('makeMarkdown.node')(children[i], options, globals); 14 | } 15 | return txt; 16 | }); 17 | -------------------------------------------------------------------------------- /src/subParsers/makemarkdown/links.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makeMarkdown.links', function (node, options, globals) { 2 | 'use strict'; 3 | 4 | var txt = ''; 5 | if (node.hasChildNodes() && node.hasAttribute('href')) { 6 | var children = node.childNodes, 7 | childrenLength = children.length; 8 | 9 | // special case for mentions 10 | // to simplify (and not make stuff really complicated) mentions will only work in this circumstance: 11 | // @user 12 | // that is, if there's a "user-mention" class and option ghMentions is true 13 | // otherwise is ignored 14 | var classes = node.getAttribute('class'); 15 | if (options.ghMentions && /(?:^| )user-mention\b/.test(classes)) { 16 | for (var ii = 0; ii < childrenLength; ++ii) { 17 | txt += showdown.subParser('makeMarkdown.node')(children[ii], options, globals); 18 | } 19 | 20 | } else { 21 | txt = '['; 22 | for (var i = 0; i < childrenLength; ++i) { 23 | txt += showdown.subParser('makeMarkdown.node')(children[i], options, globals); 24 | } 25 | txt += ']('; 26 | txt += '<' + node.getAttribute('href') + '>'; 27 | if (node.hasAttribute('title')) { 28 | txt += ' "' + node.getAttribute('title') + '"'; 29 | } 30 | txt += ')'; 31 | } 32 | 33 | 34 | } 35 | return txt; 36 | }); 37 | -------------------------------------------------------------------------------- /src/subParsers/makemarkdown/list.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makeMarkdown.list', function (node, options, globals, type) { 2 | 'use strict'; 3 | 4 | var txt = ''; 5 | if (!node.hasChildNodes()) { 6 | return ''; 7 | } 8 | var listItems = node.childNodes, 9 | listItemsLenght = listItems.length, 10 | listNum = node.getAttribute('start') || 1; 11 | 12 | for (var i = 0; i < listItemsLenght; ++i) { 13 | if (typeof listItems[i].tagName === 'undefined' || listItems[i].tagName.toLowerCase() !== 'li') { 14 | continue; 15 | } 16 | 17 | // define the bullet to use in list 18 | var bullet = ''; 19 | if (type === 'ol') { 20 | bullet = listNum.toString() + '. '; 21 | } else { 22 | bullet = '- '; 23 | } 24 | 25 | // parse list item 26 | txt += bullet + showdown.subParser('makeMarkdown.listItem')(listItems[i], options, globals); 27 | ++listNum; 28 | } 29 | 30 | return txt.trim(); 31 | }); 32 | -------------------------------------------------------------------------------- /src/subParsers/makemarkdown/listItem.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makeMarkdown.listItem', function (node, options, globals) { 2 | 'use strict'; 3 | 4 | var listItemTxt = ''; 5 | 6 | var children = node.childNodes, 7 | childrenLenght = children.length; 8 | 9 | for (var i = 0; i < childrenLenght; ++i) { 10 | listItemTxt += showdown.subParser('makeMarkdown.node')(children[i], options, globals); 11 | } 12 | // if it's only one liner, we need to add a newline at the end 13 | if (!/\n$/.test(listItemTxt)) { 14 | listItemTxt += '\n'; 15 | } else { 16 | // it's multiparagraph, so we need to indent 17 | listItemTxt = listItemTxt 18 | .split('\n') 19 | .join('\n ') 20 | .replace(/^ {4}$/gm, '') 21 | .replace(/\n\n+/g, '\n\n'); 22 | } 23 | 24 | return listItemTxt; 25 | }); 26 | -------------------------------------------------------------------------------- /src/subParsers/makemarkdown/node.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | showdown.subParser('makeMarkdown.node', function (node, options, globals, spansOnly) { 4 | 'use strict'; 5 | 6 | spansOnly = spansOnly || false; 7 | 8 | var txt = ''; 9 | 10 | // edge case of text without wrapper paragraph 11 | if (node.nodeType === 3) { 12 | return showdown.subParser('makeMarkdown.txt')(node, options, globals); 13 | } 14 | 15 | // HTML comment 16 | if (node.nodeType === 8) { 17 | return '\n\n'; 18 | } 19 | 20 | // process only node elements 21 | if (node.nodeType !== 1) { 22 | return ''; 23 | } 24 | 25 | var tagName = node.tagName.toLowerCase(); 26 | 27 | switch (tagName) { 28 | 29 | // 30 | // BLOCKS 31 | // 32 | case 'h1': 33 | if (!spansOnly) { txt = showdown.subParser('makeMarkdown.header')(node, options, globals, 1) + '\n\n'; } 34 | break; 35 | case 'h2': 36 | if (!spansOnly) { txt = showdown.subParser('makeMarkdown.header')(node, options, globals, 2) + '\n\n'; } 37 | break; 38 | case 'h3': 39 | if (!spansOnly) { txt = showdown.subParser('makeMarkdown.header')(node, options, globals, 3) + '\n\n'; } 40 | break; 41 | case 'h4': 42 | if (!spansOnly) { txt = showdown.subParser('makeMarkdown.header')(node, options, globals, 4) + '\n\n'; } 43 | break; 44 | case 'h5': 45 | if (!spansOnly) { txt = showdown.subParser('makeMarkdown.header')(node, options, globals, 5) + '\n\n'; } 46 | break; 47 | case 'h6': 48 | if (!spansOnly) { txt = showdown.subParser('makeMarkdown.header')(node, options, globals, 6) + '\n\n'; } 49 | break; 50 | 51 | case 'p': 52 | if (!spansOnly) { txt = showdown.subParser('makeMarkdown.paragraph')(node, options, globals) + '\n\n'; } 53 | break; 54 | 55 | case 'blockquote': 56 | if (!spansOnly) { txt = showdown.subParser('makeMarkdown.blockquote')(node, options, globals) + '\n\n'; } 57 | break; 58 | 59 | case 'hr': 60 | if (!spansOnly) { txt = showdown.subParser('makeMarkdown.hr')(node, options, globals) + '\n\n'; } 61 | break; 62 | 63 | case 'ol': 64 | if (!spansOnly) { txt = showdown.subParser('makeMarkdown.list')(node, options, globals, 'ol') + '\n\n'; } 65 | break; 66 | 67 | case 'ul': 68 | if (!spansOnly) { txt = showdown.subParser('makeMarkdown.list')(node, options, globals, 'ul') + '\n\n'; } 69 | break; 70 | 71 | case 'precode': 72 | if (!spansOnly) { txt = showdown.subParser('makeMarkdown.codeBlock')(node, options, globals) + '\n\n'; } 73 | break; 74 | 75 | case 'pre': 76 | if (!spansOnly) { txt = showdown.subParser('makeMarkdown.pre')(node, options, globals) + '\n\n'; } 77 | break; 78 | 79 | case 'table': 80 | if (!spansOnly) { txt = showdown.subParser('makeMarkdown.table')(node, options, globals) + '\n\n'; } 81 | break; 82 | 83 | // 84 | // SPANS 85 | // 86 | case 'code': 87 | txt = showdown.subParser('makeMarkdown.codeSpan')(node, options, globals); 88 | break; 89 | 90 | case 'em': 91 | case 'i': 92 | txt = showdown.subParser('makeMarkdown.emphasis')(node, options, globals); 93 | break; 94 | 95 | case 'strong': 96 | case 'b': 97 | txt = showdown.subParser('makeMarkdown.strong')(node, options, globals); 98 | break; 99 | 100 | case 'del': 101 | txt = showdown.subParser('makeMarkdown.strikethrough')(node, options, globals); 102 | break; 103 | 104 | case 'a': 105 | txt = showdown.subParser('makeMarkdown.links')(node, options, globals); 106 | break; 107 | 108 | case 'img': 109 | txt = showdown.subParser('makeMarkdown.image')(node, options, globals); 110 | break; 111 | 112 | case 'br': 113 | txt = showdown.subParser('makeMarkdown.break')(node, options, globals); 114 | break; 115 | 116 | case 'input': 117 | txt = showdown.subParser('makeMarkdown.input')(node, options, globals); 118 | break; 119 | 120 | default: 121 | txt = node.outerHTML + '\n\n'; 122 | } 123 | 124 | // common normalization 125 | // TODO eventually 126 | 127 | return txt; 128 | }); 129 | -------------------------------------------------------------------------------- /src/subParsers/makemarkdown/paragraph.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makeMarkdown.paragraph', function (node, options, globals) { 2 | 'use strict'; 3 | 4 | var txt = ''; 5 | if (node.hasChildNodes()) { 6 | var children = node.childNodes, 7 | childrenLength = children.length; 8 | for (var i = 0; i < childrenLength; ++i) { 9 | txt += showdown.subParser('makeMarkdown.node')(children[i], options, globals); 10 | } 11 | } 12 | 13 | // some text normalization 14 | txt = txt.trim(); 15 | 16 | return txt; 17 | }); 18 | -------------------------------------------------------------------------------- /src/subParsers/makemarkdown/pre.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makeMarkdown.pre', function (node, options, globals) { 2 | 'use strict'; 3 | 4 | var num = node.getAttribute('prenum'); 5 | return '
    ' + globals.preList[num] + '
    '; 6 | }); 7 | -------------------------------------------------------------------------------- /src/subParsers/makemarkdown/strikethrough.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makeMarkdown.strikethrough', function (node, options, globals) { 2 | 'use strict'; 3 | 4 | var txt = ''; 5 | if (node.hasChildNodes()) { 6 | txt += '~~'; 7 | var children = node.childNodes, 8 | childrenLength = children.length; 9 | for (var i = 0; i < childrenLength; ++i) { 10 | txt += showdown.subParser('makeMarkdown.node')(children[i], options, globals); 11 | } 12 | txt += '~~'; 13 | } 14 | return txt; 15 | }); 16 | -------------------------------------------------------------------------------- /src/subParsers/makemarkdown/strong.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makeMarkdown.strong', function (node, options, globals) { 2 | 'use strict'; 3 | 4 | var txt = ''; 5 | if (node.hasChildNodes()) { 6 | txt += '**'; 7 | var children = node.childNodes, 8 | childrenLength = children.length; 9 | for (var i = 0; i < childrenLength; ++i) { 10 | txt += showdown.subParser('makeMarkdown.node')(children[i], options, globals); 11 | } 12 | txt += '**'; 13 | } 14 | return txt; 15 | }); 16 | -------------------------------------------------------------------------------- /src/subParsers/makemarkdown/table.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makeMarkdown.table', 2 | /** 3 | * 4 | * @param {DocumentFragment} node 5 | * @param {{}} options 6 | * @param {{}} globals 7 | * @returns {string} 8 | */ 9 | function (node, options, globals) { 10 | 'use strict'; 11 | 12 | var txt = '', 13 | tableArray = [[], []], 14 | headings, 15 | rows = [], 16 | colCount, 17 | i, 18 | ii; 19 | 20 | /** 21 | * @param {Element} tr 22 | */ 23 | function iterateRow (tr) { 24 | var children = tr.childNodes, 25 | cols = []; 26 | // we need to iterate by order, since td and th can be used interchangeably and in any order 27 | // we will ignore malformed stuff, comments and floating text. 28 | for (var i = 0; i < children.length; ++i) { 29 | var childName = children[i].nodeName.toUpperCase(); 30 | if (childName === 'TD' || childName === 'TH') { 31 | cols.push(children[i]); 32 | } 33 | } 34 | return cols; 35 | } 36 | 37 | 38 | // first lets look for 39 | // we will ignore thead without children 40 | // also, since markdown doesn't support tables with multiple heading rows, only the first one will be transformed 41 | // the rest will count as regular rows 42 | if (node.querySelectorAll(':scope>thead').length !== 0 && node.querySelectorAll(':scope>thead>tr').length !== 0) { 43 | var thead = node.querySelectorAll(':scope>thead>tr'); 44 | 45 | // thead>tr can have td and th children 46 | for (i = 0; i < thead.length; ++i) { 47 | rows.push(iterateRow(thead[i])); 48 | } 49 | } 50 | 51 | // now let's look for tbody 52 | // we will ignore tbody without children 53 | if (node.querySelectorAll(':scope>tbody').length !== 0 && node.querySelectorAll(':scope>tbody>tr').length !== 0) { 54 | var tbody = node.querySelectorAll(':scope>tbody>tr'); 55 | // tbody>tr can have td and th children, although th are not very screen reader friendly 56 | for (i = 0; i < tbody.length; ++i) { 57 | rows.push(iterateRow(tbody[i])); 58 | } 59 | } 60 | 61 | // now look for tfoot 62 | if (node.querySelectorAll(':scope>tfoot').length !== 0 && node.querySelectorAll(':scope>tfoot>tr').length !== 0) { 63 | var tfoot = node.querySelectorAll(':scope>tfoot>tr'); 64 | // tfoot>tr can have td and th children, although th are not very screen reader friendly 65 | for (i = 0; i < tfoot.length; ++i) { 66 | rows.push(iterateRow(tfoot[i])); 67 | } 68 | } 69 | 70 | // lastly look for naked tr 71 | if (node.querySelectorAll(':scope>tr').length !== 0) { 72 | 73 | var tr = node.querySelectorAll(':scope>tr'); 74 | // tfoot>tr can have td and th children, although th are not very screen reader friendly 75 | for (i = 0; i < tr.length; ++i) { 76 | rows.push(iterateRow(tr[i])); 77 | } 78 | } 79 | 80 | // TODO: implement in tables https://developer.mozilla.org/pt-BR/docs/Web/HTML/Element/caption 81 | // note: is ignored, since they are basically styling 82 | 83 | // we need now to account for cases of completely empty tables, like
    or equivalent 84 | if (rows.length === 0) { 85 | // table is empty, return empty text 86 | return txt; 87 | } 88 | 89 | // count the first row. We need it to trim the table (if table rows have inconsistent number of columns) 90 | colCount = rows[0].length; 91 | 92 | // let's shift the first row as a heading 93 | headings = rows.shift(); 94 | 95 | for (i = 0; i < headings.length; ++i) { 96 | var headContent = showdown.subParser('makeMarkdown.tableCell')(headings[i], globals), 97 | align = '---'; 98 | 99 | if (headings[i].hasAttribute('style')) { 100 | var style = headings[i].getAttribute('style').toLowerCase().replace(/\s/g, ''); 101 | switch (style) { 102 | case 'text-align:left;': 103 | align = ':---'; 104 | break; 105 | case 'text-align:right;': 106 | align = '---:'; 107 | break; 108 | case 'text-align:center;': 109 | align = ':---:'; 110 | break; 111 | } 112 | } 113 | tableArray[0][i] = headContent.trim(); 114 | tableArray[1][i] = align; 115 | } 116 | 117 | // now iterate through the rows and create the pseudo output (not pretty yet) 118 | for (i = 0; i < rows.length; ++i) { 119 | var r = tableArray.push([]) - 1; 120 | 121 | for (ii = 0; ii < colCount; ++ii) { 122 | var cellContent = ' '; 123 | if (typeof rows[i][ii] !== 'undefined') { 124 | // Note: if rows[i][ii] is undefined, it means the row has fewer elements than the header, 125 | // and empty content will be added 126 | cellContent = showdown.subParser('makeMarkdown.tableCell')(rows[i][ii], globals); 127 | } 128 | tableArray[r].push(cellContent); 129 | } 130 | } 131 | 132 | // now tidy up the output, aligning cells and stuff 133 | var cellSpacesCount = 3; 134 | for (i = 0; i < tableArray.length; ++i) { 135 | for (ii = 0; ii < tableArray[i].length; ++ii) { 136 | var strLen = tableArray[i][ii].length; 137 | if (strLen > cellSpacesCount) { 138 | cellSpacesCount = strLen; 139 | } 140 | } 141 | } 142 | 143 | for (i = 0; i < tableArray.length; ++i) { 144 | for (ii = 0; ii < tableArray[i].length; ++ii) { 145 | if (i === 1) { 146 | if (tableArray[i][ii].slice(-1) === ':') { 147 | tableArray[i][ii] = showdown.helper.padEnd(tableArray[i][ii].slice(0, -1), cellSpacesCount - 1, '-') + ':'; 148 | } else { 149 | tableArray[i][ii] = showdown.helper.padEnd(tableArray[i][ii], cellSpacesCount, '-'); 150 | } 151 | 152 | } else { 153 | tableArray[i][ii] = showdown.helper.padEnd(tableArray[i][ii], cellSpacesCount); 154 | } 155 | } 156 | txt += '| ' + tableArray[i].join(' | ') + ' |\n'; 157 | } 158 | 159 | return txt.trim(); 160 | } 161 | ); 162 | -------------------------------------------------------------------------------- /src/subParsers/makemarkdown/tableCell.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makeMarkdown.tableCell', function (node, options, globals) { 2 | 'use strict'; 3 | 4 | var txt = ''; 5 | if (!node.hasChildNodes()) { 6 | return ''; 7 | } 8 | var children = node.childNodes, 9 | childrenLength = children.length; 10 | 11 | for (var i = 0; i < childrenLength; ++i) { 12 | txt += showdown.subParser('makeMarkdown.node')(children[i], options, globals, true); 13 | } 14 | return txt.trim(); 15 | }); 16 | -------------------------------------------------------------------------------- /src/subParsers/makemarkdown/txt.js: -------------------------------------------------------------------------------- 1 | showdown.subParser('makeMarkdown.txt', function (node) { 2 | 'use strict'; 3 | 4 | var txt = node.nodeValue; 5 | 6 | // multiple spaces are collapsed 7 | txt = txt.replace(/ +/g, ' '); 8 | 9 | // replace the custom ¨NBSP; with a space 10 | txt = txt.replace(/¨NBSP;/g, ' '); 11 | 12 | // ", <, > and & should replace escaped html entities 13 | txt = showdown.helper.unescapeHTMLEntities(txt); 14 | 15 | // escape markdown magic characters 16 | // emphasis, strong and strikethrough - can appear everywhere 17 | // we also escape pipe (|) because of tables 18 | // and escape ` because of code blocks and spans 19 | txt = txt.replace(/([*_~|`])/g, '\\$1'); 20 | 21 | // escape > because of blockquotes 22 | txt = txt.replace(/^(\s*)>/g, '\\$1>'); 23 | 24 | // hash character, only troublesome at the beginning of a line because of headers 25 | txt = txt.replace(/^#/gm, '\\#'); 26 | 27 | // horizontal rules 28 | txt = txt.replace(/^(\s*)([-=]{3,})(\s*)$/, '$1\\$2$3'); 29 | 30 | // dot, because of ordered lists, only troublesome at the beginning of a line when preceded by an integer 31 | txt = txt.replace(/^( {0,3}\d+)\./gm, '$1\\.'); 32 | 33 | // +, * and -, at the beginning of a line becomes a list, so we need to escape them also (asterisk was already escaped) 34 | txt = txt.replace(/^( {0,3})([+-])/gm, '$1\\$2'); 35 | 36 | // images and links, ] followed by ( is problematic, so we escape it 37 | txt = txt.replace(/]([\s]*)\(/g, '\\]$1\\('); 38 | 39 | // reference URIs must also be escaped 40 | txt = txt.replace(/^ {0,3}\[([\S \t]*?)]:/gm, '\\[$1]:'); 41 | 42 | return txt; 43 | }); 44 | --------------------------------------------------------------------------------