Merge branch 'develop' into feat/910-auto-linking-mention-from-html-to-markdown

# Conflicts:
#	src/subParsers/makemarkdown/table.js
This commit is contained in:
Estevão Soares dos Santos 2022-04-16 20:09:50 +01:00
commit 3c2e25a3c7
49 changed files with 4231 additions and 338 deletions

View File

@ -1,4 +1,7 @@
{ {
"env": {
"es6": true
},
"rules": { "rules": {
"indent": [2, 2, {"SwitchCase": 1, "VariableDeclarator": 2}], "indent": [2, 2, {"SwitchCase": 1, "VariableDeclarator": 2}],
"curly": [2, "all"], "curly": [2, "all"],

63
.github/workflows/browserstack.yml vendored Normal file
View File

@ -0,0 +1,63 @@
name: 'BrowserStack Test'
on:
push:
branches: [ master, develop ]
pull_request:
branches: [ master, develop ]
jobs:
ubuntu-job:
name: 'BrowserStack Test on Ubuntu'
runs-on: ubuntu-latest
steps:
- name: set up env vars
# Only the first line of commit msg
run: echo "COMMIT_MSG=$(printf "%s" "${{ github.event.head_commit.message }}" | head -n 1)" >> $GITHUB_ENV
- name: '📦 Checkout the repository'
uses: actions/checkout@v2
- name: '🚚 Upgrade NPM'
run: npm install -g npm
- name: '⚙ Setup Node.js v17.x'
uses: actions/setup-node@v2
with:
node-version: 17.x
cache: 'npm'
- name: '📖 Get current package version'
id: package-version
uses: martinbeentjes/npm-get-version-action@v1.1.0
- name: '📝 Print build version and commit msg'
run: 'printf "version: %s\n build:%s\n message:%s\n" "${{ steps.package-version.outputs.current-version}}" "${{ github.run_id }}" "$COMMIT_MSG"'
- name: '📱 BrowserStack Env Setup' # Invokes the setup-env action
uses: browserstack/github-actions/setup-env@master
with:
username: ${{ secrets.BROWSERSTACK_USERNAME }}
access-key: ${{ secrets.BROWSERSTACK_ACCESSKEY }}
project-name: 'showdown'
build-name: ${{ steps.package-version.outputs.current-version}}-${{ github.run_id }}
- name: '🚇 BrowserStack Local Tunnel Setup' # Invokes the setup-local action
uses: browserstack/github-actions/setup-local@master
with:
local-testing: start
local-identifier: random
- name: '🚚 Install dependencies for CI'
run: npm ci
- name: '🏗 Building src files for testing'
run: npx grunt concat:test
- name: '✅ Running test on BrowserStack with Karma'
run: npx karma start karma.browserstack.js
- name: '🛑 BrowserStackLocal Stop' # Terminating the BrowserStackLocal tunnel connection
uses: browserstack/github-actions/setup-local@master
with:
local-testing: stop

View File

@ -2,14 +2,12 @@ name: documentation
on: on:
push: push:
branches: branches:
- master - master
paths: paths:
- 'mkdocs.yml' - 'mkdocs.yml'
- 'docs/**' - 'docs/**'
- '.github/workflows/docs.yml' - '.github/workflows/docs.yml'
paths-ignore:
- 'README.md'
jobs: jobs:
build_docs: build_docs:
@ -30,4 +28,4 @@ jobs:
-H "Authorization: token ${TOKEN}" \ -H "Authorization: token ${TOKEN}" \
-H "Accept: application/vnd.github.v3+json" \ -H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/${OWNER}/${REPO}/dispatches \ https://api.github.com/repos/${OWNER}/${REPO}/dispatches \
-d '{ "event_type": "e: \"'"${COMMIT}"'\" by '"${COMMITTER}"'", "client_payload": { "source": "showdown" } }' -d '{ "event_type": "e: \"'"${COMMIT}"'\" by '"${COMMITTER}"'", "client_payload": { "source": "showdown" } }'

1
.gitignore vendored
View File

@ -4,3 +4,4 @@
node_modules node_modules
npm-debug.log npm-debug.log
/*.test.* /*.test.*
*.log

View File

@ -3,3 +3,4 @@ dist/**/*.js
build/**/*.js build/**/*.js
src/options.js src/options.js
bin/* bin/*
/karma.browserstack.js

View File

@ -19,7 +19,6 @@
"smarttabs": true, "smarttabs": true,
"onevar": true, "onevar": true,
"globals": { "globals": {
"angular": true,
"module": true, "module": true,
"define": true, "define": true,
"window": true, "window": true,

View File

@ -8,6 +8,16 @@ module.exports = function (grunt) {
require('quiet-grunt'); require('quiet-grunt');
} }
/**
* Load common tasks for legacy and normal tests
*/
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-mocha-test');
grunt.loadNpmTasks('grunt-endline');
grunt.loadNpmTasks('grunt-contrib-jshint');
// Project configuration. // Project configuration.
var config = { var config = {
pkg: grunt.file.readJSON('package.json'), pkg: grunt.file.readJSON('package.json'),
@ -124,40 +134,40 @@ module.exports = function (grunt) {
} }
}, },
simplemocha: { mochaTest: {
functional: { functional: {
src: 'test/functional/**/*.js', src: 'test/functional/**/*.js',
options: { options: {
globals: ['should'],
timeout: 3000, timeout: 3000,
ignoreLeaks: true, ignoreLeaks: true,
reporter: 'spec' reporter: 'spec',
require: ['test/bootstrap.js']
} }
}, },
unit: { unit: {
src: 'test/unit/**/*.js', src: 'test/unit/**/*.js',
options: { options: {
globals: ['should'],
timeout: 3000, timeout: 3000,
ignoreLeaks: true, ignoreLeaks: true,
reporter: 'spec' reporter: 'spec',
require: ['test/bootstrap.js']
} }
}, },
single: { single: {
options: { options: {
globals: ['should'],
timeout: 3000, timeout: 3000,
ignoreLeaks: false, ignoreLeaks: false,
reporter: 'spec' reporter: 'spec',
require: ['test/bootstrap.js']
} }
}, },
cli: { cli: {
src: 'test/unit/cli.js', src: 'test/unit/cli.js',
options: { options: {
globals: ['should'],
timeout: 3000, timeout: 3000,
ignoreLeaks: false, ignoreLeaks: false,
reporter: 'spec' reporter: 'spec',
require: ['test/bootstrap.js']
} }
} }
} }
@ -165,16 +175,6 @@ module.exports = function (grunt) {
grunt.initConfig(config); grunt.initConfig(config);
/**
* Load common tasks for legacy and normal tests
*/
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-simple-mocha');
grunt.loadNpmTasks('grunt-endline');
grunt.loadNpmTasks('grunt-contrib-jshint');
/** /**
* Generate Changelog * Generate Changelog
*/ */
@ -210,23 +210,23 @@ module.exports = function (grunt) {
grunt.registerTask('single-test', function (file) { grunt.registerTask('single-test', function (file) {
'use strict'; 'use strict';
grunt.config.merge({ grunt.config.merge({
simplemocha: { mochaTest: {
single: { single: {
src: file src: file
} }
} }
}); });
grunt.task.run(['lint', 'concat:test', 'simplemocha:single', 'clean']); grunt.task.run(['lint', 'concat:test', 'mochaTest:single', 'clean']);
}); });
/** /**
* Tasks * Tasks
*/ */
grunt.registerTask('test', ['clean', 'lint', 'concat:test', 'simplemocha:unit', 'simplemocha:functional', 'clean']); grunt.registerTask('test', ['clean', 'lint', 'concat:test', 'mochaTest:unit', 'mochaTest:functional', 'clean']);
grunt.registerTask('test-functional', ['concat:test', 'simplemocha:functional', 'clean']); grunt.registerTask('test-functional', ['concat:test', 'mochaTest:functional', 'clean']);
grunt.registerTask('test-unit', ['concat:test', 'simplemocha:unit', 'clean']); grunt.registerTask('test-unit', ['concat:test', 'mochaTest:unit', 'clean']);
grunt.registerTask('test-cli', ['clean', 'lint', 'concat:test', 'simplemocha:cli', 'clean']); grunt.registerTask('test-cli', ['clean', 'lint', 'concat:test', 'mochaTest:cli', 'clean']);
grunt.registerTask('performance', ['concat:test', 'performancejs', 'clean']); grunt.registerTask('performance', ['concat:test', 'performancejs', 'clean']);
grunt.registerTask('build', ['test', 'concat:dist', 'concat:cli', 'uglify:dist', 'uglify:cli', 'endline']); grunt.registerTask('build', ['test', 'concat:dist', 'concat:cli', 'uglify:dist', 'uglify:cli', 'endline']);

View File

@ -2,9 +2,9 @@
![Build Status: Linux](https://github.com/showdownjs/showdown/actions/workflows/node.linux.yml/badge.svg) ![Build Status: Linux](https://github.com/showdownjs/showdown/actions/workflows/node.linux.yml/badge.svg)
![Build Status: Windows](https://github.com/showdownjs/showdown/actions/workflows/node.win.yml/badge.svg) ![Build Status: Windows](https://github.com/showdownjs/showdown/actions/workflows/node.win.yml/badge.svg)
[![Browserstack Tests](https://automate.browserstack.com/badge.svg?badge_key=VTIvTDNqWVdaTHljbS9RNmYrcTBiL0Uxc3dkRDhaN1dPaXpPb2VOc1B2VT0tLU1Ib09kVjVzMjhFcHExbWFSWlJEV3c9PQ==--1fb92e1730e4a00630d17d533822de6403ca65ec)](https://automate.browserstack.com/public-build/VTIvTDNqWVdaTHljbS9RNmYrcTBiL0Uxc3dkRDhaN1dPaXpPb2VOc1B2VT0tLU1Ib09kVjVzMjhFcHExbWFSWlJEV3c9PQ==--1fb92e1730e4a00630d17d533822de6403ca65ec)
[![npm version](https://badge.fury.io/js/showdown.svg)](http://badge.fury.io/js/showdown) [![npm version](https://badge.fury.io/js/showdown.svg)](http://badge.fury.io/js/showdown)
[![Bower version](https://badge.fury.io/bo/showdown.svg)](http://badge.fury.io/bo/showdown) [![Bower version](https://badge.fury.io/bo/showdown.svg)](http://badge.fury.io/bo/showdown)
[![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)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/tiviesantos) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/tiviesantos)
------ ------

View File

@ -101,12 +101,12 @@
## CLI ## CLI
- [ ] Refactor the CLI - [ ] Refactor the CLI
- [ ] **#381**: *Support for src and dst directories in showdown cli* - [ ] **#381**: *Support for src and dst directories in showdown cli*
- [ ] **#584**: *Fails to read from stdin* - [X] **#584**: *Fails to read from stdin*
- [ ] **#554**: *CLI not working with jsdom v10* - [X] **#554**: *CLI not working with jsdom v10*
## Other stuff ## Other stuff
- [X] Regexp rewrite for more performance oompf - [X] Regexp rewrite for more performance oompf
- [ ] Full unit testing - [X] Full unit testing
- [ ] Better error reporting - [ ] Better error reporting
## Stuff that probably won't make it to v2.0 ## Stuff that probably won't make it to v2.0
@ -138,3 +138,9 @@ This should address:
- [ ] Options - [ ] Options
- [ ] Extensions (and the new event system) - [ ] Extensions (and the new event system)
- [ ] Cookbook (with stuff for backwards compatibility, specially regarding removed options) - [ ] Cookbook (with stuff for backwards compatibility, specially regarding removed options)
## Browser Testing
- [X] Implement unit tests in Karma
- [ ] Implement functional tests in Karma
- [ ] Integrate with browserstack

BIN
dist/showdown.js vendored

Binary file not shown.

773
docs/available-options.md Normal file
View File

@ -0,0 +1,773 @@
!!! warning ""
Starting from the version `1.6.0` and earlier, all the options are `disabled` by default in the cli tool.
### backslashEscapesHTMLTags
Support escaping of HTML tags.
* type: `boolean`
* default value: `false`
* introduced in: `1.7.2`
=== "input"
```html
\<div>foo\</div>
```
=== "output (value is `true`)"
```html
<p>&lt;div&gt;foo&lt;/div&gt;</p>
```
### completeHTMLDocument
Output a complete HTML document, including `<html>`, `<head>`, and `<body>` tags instead of an HTML fragment.
* type: `boolean`
* default value: `false`
* introduced in: `1.8.5`
### customizedHeaderId
Set custom ID for a heading.
!!! warning ""
This option can be overridden with the [`noHeaderId`](#noheaderid) option.
* type: `boolean`
* default value: `false`
* introduced in: `1.7.0`
=== "code"
```html
## Sample heading {mycustomid}
```
=== "output"
```html
<h1 id="mycustomid">This is a heading</h1>
```
!!! hint ""
For better readability and human-friendliness of the heading IDs, it is also recommended to set the [`ghCompatibleHeaderId`](#ghcompatibleheaderid) option to `true`.
### disableForced4SpacesIndentedSublists
Disable the rule of 4 spaces to indent sub-lists. If enabled, this option effectively reverts to the old behavior where you can indent sub-lists with 2 or 3 spaces.
* type: `boolean`
* default value: `false`
* introduced in: `1.5.0`
=== "input"
```
- one
- two
...
- one
- two
```
=== "output (value is `false`)"
```html
<ul>
<li>one</li>
<li>two</li>
</ul>
<p>...</p>
<ul>
<li>one
<ul>
<li>two</li>
</ul>
</li>
</ul>
```
=== "output (value is `true`)"
```html
<ul>
<li>one
<ul>
<li>two</li>
</ul>
</li>
</ul>
<p>...</p>
<ul>
<li>one
<ul>
<li>two</li>
</ul>
</li>
</ul>
```
### emoji
Enable emoji support. For more info on available emojis, see https://github.com/showdownjs/showdown/wiki/Emojis (since v.1.8.0)
* type: `boolean`
* default value: `false`
* introduced in: `1.8.0`
=== "input"
```
this is a :smile: emoji
```
=== "output (value is `false`)"
```html
<p>this is a :smile: emoji</p>
```
=== "output (value is `true`)"
```html
<p>this is a 😄 emoji</p>
```
!!! hint "Full list of supported emojies"
Check the [Showdown Wiki](https://github.com/showdownjs/showdown/wiki/Emojis#emoji-list) for a full list of supported emojies.
### encodeEmails
Enable automatic obfuscation of email addresses. During this process, email addresses are encoded via Character Entities, transforming ASCII email addresses into their equivalent decimal entities.
* type: `boolean`
* default value: `false`
* introduced in: `1.6.1`
=== "input"
```
<myself@example.com>
```
=== "output (value is `false`)"
```html
<a href="mailto:myself@example.com">myself@example.com</a>
```
=== "output (value is `true`)"
```html
<a href="&#109;&#97;&#105;&#108;t&#x6f;&#x3a;&#109;&#x79;s&#x65;&#x6c;&#102;&#64;&#x65;xa&#109;&#112;&#108;&#101;&#x2e;c&#x6f;&#109;">&#x6d;&#121;s&#101;&#108;f&#x40;&#x65;&#120;a&#x6d;&#x70;&#108;&#x65;&#x2e;&#99;&#x6f;&#109;</a>
```
### excludeTrailingPunctuationFromURLs
Exclude trailing punctuation from autolinked URLs: `.` `!` `?` `(` `)`
This option applies only to links generated by [`simplifiedAutoLink`](#simplifiedautolink).
* type: `boolean`
* default value: `false`
* introduced in: `1.5.1`
=== "input"
```
check this link www.google.com.
```
=== "output (value is `false`)"
```html
<p>check this link <a href="www.google.com">www.google.com.</a></p>
```
=== "output (value is `true`)"
```html
<p>check this link <a href="www.google.com">www.google.com</a>.</p>
```
### ghCodeBlocks
Enable support for GFM code block style syntax (fenced codeblocks).
* type: `boolean`
* default value: `true`
* introduced in: `0.3.1`
=== "example"
```
```
some code here
```
```
### ghCompatibleHeaderId
Generate heading IDs compatible with GitHub style: spaces are replaced with dashes, and certain non-alphanumeric chars are removed.
!!! warning ""
This option can be overridden with the [`noHeaderId`](#noheaderid) option.
* type: `boolean`
* default value: `false`
* introduced in: `1.5.5`
=== "input"
```
# This is a heading with @#$%
```
=== "output (value is `false`)"
```html
<h1 id="thisisaheading">This is a heading</h1>
```
=== "output (value is `true`)"
```html
<h1 id="this-is-a-heading-with-">This is a heading with @#$%</h1>
```
### ghMentions
Enables support for GitHub `@mentions` that allows you to link to the GitHub profile page of the mentioned username.
* type: `boolean`
* default value: `false`
* introduced in: `1.6.0`
=== "input"
```
hello there @tivie
```
=== "output (value is `false`)"
```html
<p>hello there @tivie</p>
```
=== "output (value is `true`)"
```html
<p>hello there <a href="https://www.github.com/tivie">@tivie</a></p>
```
### ghMentionsLink
Specify where the link generated by `@mentions` should point to. Works only when [`ghMentions: true`](#ghmentions).
* type: `boolean`
* default value: `https://github.com/{u}`
* introduced in: `1.6.2`
=== "input"
```
hello there @tivie
```
=== "output (value is `https://github.com/{u}`)"
```html
<p>hello there <a href="https://www.github.com/tivie">@tivie</a></p>
```
=== "output (value is `http://mysite.com/{u}/profile`)"
```html
<p>hello there <a href="//mysite.com/tivie/profile">@tivie</a></p>
```
### headerLevelStart
Set starting level for the heading tags.
* type: `integer`
* default value: `1`
* introduced in: `1.1.0`
=== "input"
```
# This is a heading
```
=== "output (value is `1`)"
```html
<h1>This is a heading</h1>
```
=== "output (value is `3`)"
```html
<h3>This is a heading</h3>
```
### literalMidWordUnderscores
Treat underscores in the middle of words as literal characters.
Underscores allow you to specify the words that should be emphasized. However, in some cases, this may be unwanted behavior. With this option enabled, underscores in the middle of words will no longer be interpreted as `<em>` and `<strong>`, but as literal underscores.
* type: `boolean`
* default value: `false`
* introduced in: `1.2.0`
=== "input"
```
some text with__underscores__in the middle
```
=== "output (value is `false`)"
```html
<p>some text with<strong>underscores</strong>in the middle</p>
```
=== "output (value is `true`)"
```html
<p>some text with__underscores__in the middle</p>
```
### metadata
Enable support for document metadata (front-matter). You can define metadata at the top of a document between `««« »»»` or `--- ---` symbols.
* type: `boolean`
* default value: `false`
* introduced in: `1.8.5`
=== "input"
```js
let ref = `referenced value`;
var markdown = `
---
first: Lorem
second: Ipsum
ref_variable: ${ref}
---
`
var conv = new showdown.Converter({metadata: true});
var html = conv.makeHtml(markdown);
var metadata = conv.getMetadata();
```
=== "output (value is `true`)"
```js
// console.log(metadata)
{
first: 'Lorem',
second: 'Ipsum',
ref_variable: 'referenced value'
}
```
### noHeaderId
Disable automatic generation of heading IDs.
!!! warning ""
Setting the option to `true` overrides the following options:
* [`prefixHeaderId`](#prefixheaderid)
* [`customizedHeaderId`](#customizedheaderid)
* [`ghCompatibleHeaderId`](#ghcompatibleheaderid)
* type: `boolean`
* default value: `false`
* introduced in: `1.1.0`
=== "input"
```
# This is a heading
```
=== "output (value is `false`)"
```html
<h1 id="thisisaheading">This is a heading</h1>
```
=== "output (value is `true`)"
```html
<h1>This is a heading</h1>
```
### omitExtraWLInCodeBlocks
Omit trailing newline in code blocks (which is set by default before the closing tag). This option affects both indented and fenced (gfm style) code blocks.
* type: `boolean`
* default value: `false`
* introduced in: `1.0.0`
=== "input"
```
var foo = 'bar';
```
=== "output (value is `false`)"
```html
<code><pre>var foo = 'bar';
</pre></code>
```
=== "output (value is `true`)"
```html
<code><pre>var foo = 'bar';</pre></code>
```
### openLinksInNewWindow
Open links in new windows.
* type: `boolean`
* default value: `false`
* introduced in: `1.7.0`
=== "input"
```
[link](https://google.com)
```
=== "output (value is `false`)"
```html
<a href="https://google.com">link</a>
```
=== "output (value is `true`)"
```html
<a href="https://google.com" rel="noopener noreferrer" target="_blank">link</a>
```
### parseImgDimensions
Set image dimensions from within Markdown syntax.
* type: `boolean`
* default value: `false`
* introduced in: `1.1.0`
=== "example"
```
![foo](foo.jpg =100x80) set width to 100px and height to 80px
![bar](bar.jpg =100x*) set width to 100px and height to "auto"
![baz](baz.jpg =80%x5em) set width to 80% and height to 5em
```
### prefixHeaderId
Add a prefix to the generated heading ID:
* Passing a string will add that string to the heading ID.
* Passing `true` will add a generic `section` prefix.
!!! warning ""
This option can be overridden with the [`noHeaderId`](#noheaderid) option.
* type: `string / boolean`
* default value: `false`
=== "input"
```
# This is a heading
```
=== "output (value is `false`)"
```html
<h1 id="thisisaheading">This is a heading</h1>
```
=== "output (value is `true`)"
```html
<h1 id="sectionthisisaheading">This is a heading</h1>
```
=== "output (value is `showdown`)"
```html
<h1 id="showdownthisisaheading">This is a heading</h1>
```
### rawHeaderId
Replace ` ` (space), `'` (single quote), and `"` (double quote) with `-` (dash) in the generated heading IDs, including prefixes.
!!! danger ""
**Use with caution** as it might result in malformed IDs.
* type:
* default value:
* introduced in: `1.7.3`
### rawPrefixHeaderId
Prevent Showndown from modifying the prefix. Works only when [`prefixHeaderId`](#prefixheaderid) is set to a string value.
!!! danger ""
**Use with caution** as it might result in malformed IDs. For example, when the prefix contains special characters like `"` `\` `/` or others.
* type: `boolean`
* default value: `false`
* introduced in: `1.7.3`
### requireSpaceBeforeHeadingText
Require a space between a heading `#` and the heading text.
* type: `boolean`
* default value: `false`
* introduced in: `1.5.3`
=== "input"
```
#heading
```
=== "output (value is `false`)"
```html
<h1 id="heading">heading</h1>
```
=== "output (value is `true`)"
```html
<p>#heading</p>
```
### simpleLineBreaks
Parse line breaks as `<br/>` in paragraphs (GitHub-style behavior).
* type: `boolean`
* default value: `false`
* introduced in: `1.5.1`
=== "input"
```
a line
wrapped in two
```
=== "output (value is `false`)"
```html
<p>a line
wrapped in two</p>
```
=== "output (value is `true`)"
```html
<p>a line<br>
wrapped in two</p>
```
### simplifiedAutoLink
Enable automatic linking for plain text URLs.
* type: `boolean`
* default value: `false`
* introduced in: `1.2.0`
=== "input"
```
Lorem ipsum www.google.com
```
=== "output (value is `false`)"
```html
<p>Lorem ipsum www.google.com</p>
```
=== "output (value is `true`)"
```html
<p>Lorem ipsum <a href="www.google.com">www.google.com</a></p>
```
### smartIndentationFix
Resolve indentation problems related to ES6 template strings in the midst of indented code.
* type: `boolean`
* default value: `false`
* introduced in: `1.4.2`
### smoothLivePreview
Resolve an awkward effect when a paragraph is followed by a list. This effect appears on some circumstances, in live preview editors.
* type: `boolean`
* default value: `false`
* introduced in: `1.2.1`
!!! example "awkward effect"
![](http://i.imgur.com/YQ9iHTL.gif)
### splitAdjacentBlockquotes
Split adjacent blockquote blocks.
* type: `boolean`
* default value: `false`
* introduced in: `1.8.6`
=== "input"
```
> Quote #1
>> Sub-quote 1
> Quote #2
>> Sub-quote 2
```
=== "output (value is `false`)"
```html
<blockquote>
<p>Quote #1</p>
<blockquote>
<p>Sub-quote 1</p>
</blockquote>
<p>Quote #2</p>
<blockquote>
<p>Sub-quote 2</p>
</blockquote>
</blockquote>
```
=== "output (value is `true`)"
```html
<blockquote>
<p>Quote #1</p>
<blockquote>
<p>Sub-quote 1</p>
</blockquote>
</blockquote>
<blockquote>
<p>Quote #2</p>
<blockquote>
<p>Sub-quote 2</p>
</blockquote>
</blockquote>
```
### strikethrough
Enable support for strikethrough (`<del>`).
* type: `boolean`
* default value: `false`
* introduced in: `1.2.0`
=== "input"
```
~~strikethrough~~
```
=== "output (value is `true`)"
```html
<del>strikethrough</del>
```
### tables
Enable support for tables syntax.
* type: `boolean`
* default value: `false`
* introduced in: `1.2.0`
=== "example"
```
| h1 | h2 | h3 |
|:------|:-------:|--------:|
| 100 | [a][1] | ![b][2] |
| *foo* | **bar** | ~~baz~~ |
```
### tablesHeaderId
Generate automatic IDs for table headings. Works only when [`tables: true`](#tables).
* type: `boolean`
* default value: `false`
* introduced in: `1.2.0`
### tasklists
Enable support for GitHub style tasklists.
* type: `boolean`
* default value: `false`
* introduced in: `1.2.0`
=== "example"
```
- [x] This task is done
- [ ] This task is still pending
```
### underline
Enable support for underline. If enabled, underscores will no longer be parsed as `<em>` and `<strong>`.
* type: `boolean`
* default value: `false`
* status: `Experimental`
=== "example"
```
__underlined word__ // double underscores
___underlined word___ // triple underscores
```

153
docs/cli.md Normal file
View File

@ -0,0 +1,153 @@
Showdown comes bundled with a Command-line interface (CLI) tool that allows you to run Showdown converter from the command line.
## Requirements
* [Node.js](https://nodejs.org/en/)
## Quick start guide
1. Check that Showdown CLI is accessible.
* 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:
=== "input"
```sh
showdown -h
```
=== "output"
```
Usage: showdown <command> [options]
CLI to Showdownjs markdown parser v3.0.0-alpha
Options:
-V, --version output the version number
-q, --quiet Quiet mode. Only print errors
-m, --mute Mute mode. Does not print anything
-h, --help display help for command
Commands:
makehtml [options] Converts markdown into html
help [command] display help for command
```
* 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:
=== "input"
```sh
node ./bin/showdown.js -h
```
=== "output"
```
Usage: showdown <command> [options]
CLI to Showdownjs markdown parser v3.0.0-alpha
Options:
-V, --version output the version number
-q, --quiet Quiet mode. Only print errors
-m, --mute Mute mode. Does not print anything
-h, --help display help for command
Commands:
makehtml [options] Converts markdown into html
help [command] display help for command
```
1. Use `makehtml` command to convert your document to HTML. For example:
!!! example "Convert `foo.md` into `bar.html`"
```sh
showdown makehtml -i foo.md -o bar.html
```
## Commands
### `makehtml`
Convert a Markdown input into HTML.
**Usage**
```sh
showdown makehtml [options]
```
#### Options
###### `-i / --input`
* Short format: `-i`
* Alias: `--input`
* Description: Input source. Usually a `.md` file. If omitted or empty, reads from `stdin`.
* Examples:
!!! example ""
```sh
// Read from stdin and output to stdout
showdown makehtml -i
// Read from the foo.md file and output to stdout
showdown makehtml --input foo.md
```
###### `-o/--output`
* Short format: `-o`
* Alias: `--output`
* Description: Output target. Usually a `.html` file. If omitted or empty, writes to `stdout`.
* Example:
!!! example ""
```sh
// Read from the foo.md file and output to bar.html
showdown makehtml -i foo.md -o bar.html
```
###### `-a/--append`
* Short format: `-a`
* Alias: `--append`
* Description: Append data to output instead of overwriting.
* Example:
!!! example ""
```sh
showdown makehtml -a
```
###### `-u/--encoding`
* Short format: `-u`
* Alias: `--encoding`
* Description: Specify the input encoding.
* Example:
!!! example ""
```sh
showdown makehtml -u UTF8
```
###### `-e/--extensions`
* Short format: `-e`
* Alias: `--extension`
* Description: Load the specified extension(s). Should be valid path(s) to Node-compatible extensions.
* Example:
!!! example ""
```sh
showdown makehtml -e ~/twitter.js -e ~/youtube.js
```

60
docs/configuration.md Normal file
View File

@ -0,0 +1,60 @@
You can change Showdown's default behavior via options.
## Set option
### Globally
Setting an option globally affects all Showdown instances.
```js
showdown.setOption('optionKey', 'value');
```
### Locally
Setting an option locally affects the specified Converter object only. You can set local options via:
=== "Constructor"
```js
var converter = new showdown.Converter({optionKey: 'value'});
```
=== "setOption() method"
```js
var converter = new showdown.Converter();
converter.setOption('optionKey', 'value');
```
## Get option
Showdown provides both local and global methods to retrieve previously set options:
=== "getOption()"
```js
// Global
var myOption = showdown.getOption('optionKey');
//Local
var myOption = converter.getOption('optionKey');
```
=== "getOptions()"
```js
// Global
var showdownGlobalOptions = showdown.getOptions();
//Local
var thisConverterSpecificOptions = converter.getOptions();
```
### Get default options
You can get Showdown's default options with:
```js
var defaultOptions = showdown.getDefaultOptions();
```

182
docs/create-extension.md Normal file
View File

@ -0,0 +1,182 @@
A Showdown extension is a function that returns an array of language or outputs extensions (henceforth called "sub-extensions").
```js
var myext = function () {
var myext1 = {
type: 'lang',
regex: /markdown/g,
replace: 'showdown'
};
var myext2 = {
/* extension code */
};
return [myext1, myext2];
}
```
Each sub-extension (`myext1` and `myext2` in the example above) should be an object that defines the behavior of the corresponding sub-extension.
## Sub-extension object properties
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).
### Type
**Type** is a **required** property that defines the nature of the corresponding sub-extensions. It takes one of the two values:
* **`lang`**: language extension to add new Markdown syntax to Showdown.
`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).
!!! example "When to use `lang` type"
For example, if you want the `^^youtube http://www.youtube.com/watch?v=oHg5SJYRHA0` syntax to automatically be rendered as an embedded YouTube video.
* **`output`**: output extension (or modifier) to alter the HTML output generated by Showdown.
`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.
!!! example "When to use `output` type"
For example, if you want the `<div class="header">` to become `<header>`.
### Regex and replace
`regex`/`replace` properties are similar to the Javascript's `string.replace` function and work the same way:
* `regex`: a `string` or a `RegExp` object.
If `regex` is a `string`, it will automatically be assigned a `g` (global) modifier, that is, all matches of that string will be replaced.
* `replace` a `string` or a `function`.
If `replace` is a `string`, you can use the `$1` syntax for group substitution, exactly as if it were making use of `string.replace`.
!!! example "Regex and replace example"
In this example, all the occurrences of `markdown` will be replaced with `showndown`.
```js
var myext = {
type: 'lang',
regex: /markdown/g,
replace: 'showdown'
};
```
### Filter
Alternately, if you'd like to have more control over the modification process, you can use `filter` property.
This property should be used as a function that acts as a callback. The callback should receive the following parameters:
1. `text`: the source text within the Showdown's engine.
1. `converter`: the full instance of the current Showdown's converter object.
1. `options`: the options used to initialize the converter
!!! warning ""
The filter function **should return the transformed text**. If it doesn't, it will fail **silently** and return an empty output.
!!! example "Filter example"
```js
var myext = {
type: 'lang',
filter: function (text, converter, options) {
// ... do stuff to text ...
return text;
}
};
```
!!! warning "Use `filter` with care"
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.
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**.
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:
```js
var x = 0;
var myext = {
type: 'lang',
filter: function (text, converter) {
if (x < 3) {
++x;
someSubText = converter.makeHtml(someSubText);
}
}
};
```
## Register an extension
To let Showdown know what extensions are available, you need to register them in the Showdown global object.
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.
```js
showdown.extension('myext', myext);
```
## Test an extension
The Showdown test runner is configured to automatically test cases for extensions.
To add test cases for an extension:
1. Create a new folder under `./test/extensions` that matches with the name of the `.js` file in `./src/extensions`.
1. Place any test cases into the filter using the `md/html` format. These cases will automatically be executed when running tests.
## Additional information
### Escape and normalization
Showdown performs the following escape/normalization:
* Replaces `¨` (trema) with `¨T`
* Replaces `$` (dollar sign) with `¨D`
* Normalizes line endings (`\r`, `\r\n` are converted into `\n`)
* Uses `\r` as a char placeholder
!!! note ""
This only applies to **language extensions** since these chars are unescaped before output extensions are run.
!!! warning ""
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.
### Implementation concerns
One of the concerns is maintaining both client-side and server-side compatibility. You can do this with a few lines of boilerplate code.:
```js
(function (extension) {
if (typeof showdown !== 'undefined') {
// global (browser or node.js global)
extension(showdown);
} else if (typeof define === 'function' && define.amd) {
// AMD
define(['showdown'], extension);
} else if (typeof exports === 'object') {
// Node, CommonJS-like
module.exports = extension(require('showdown'));
} else {
// showdown was not found so an error is thrown
throw Error('Could not find showdown library');
}
}(function (showdown) {
// loading extension into showdown
showdown.extension('myext', function () {
var myext = { /* ... actual extension code ... */ };
return [myext];
});
}));
```
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.
It is also loaded conditionally to make it compatible with different loading mechanisms (such as browser, CommonJS, or AMD).

36
docs/extensions.md Normal file
View File

@ -0,0 +1,36 @@
Showdown allows you to load additional functionality via extensions. You can find a list of known Showdown extensions [here][ext-wiki].
You can also check the [boilerplate repo][boilerplate-repo], to create your own extension(s).
## Usage
=== "Server-side"
```js
// Using a bundled extension
var showdown = require('showdown');
var converter = new showdown.Converter({ extensions: ['twitter'] });
// Using a custom extension
var mine = require('./custom-extensions/mine');
var converter = new showdown.Converter({ extensions: ['twitter', mine] });
```
=== "Client-side"
```js
<script src="src/showdown.js"></script>
<script src="src/extensions/twitter.js"></script>
<script>var converter = new showdown.Converter({ extensions: ['twitter'] });</script>
```
=== "CLI"
In the CLI tool, use the [`-e` flag](/cli/#-e-extensions) to load an extension.
```sh
showdown -e twitter -i foo.md -o bar.html
```
[ext-wiki]: https://github.com/showdownjs/showdown/wiki/extensions
[boilerplate-repo]: https://github.com/showdownjs/extension-boilerplate

23
docs/flavors.md Normal file
View File

@ -0,0 +1,23 @@
## Overview
You can use _flavors_ (or presets) to set the preferred options automatically. In this way, Showdown behaves like popular Markdown flavors.
Currently, the following flavors are available:
* `original`: Original Markdown flavor as in [John Gruber's spec](https://daringfireball.net/projects/markdown/)
* `vanilla`: Showdown base flavor (v1.3.1 onwards)
* `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)
## Set flavor
=== "Globally"
```js
showdown.setFlavor('github');
```
=== "Locally"
```js
converter.setFlavor('github');
```

20
docs/integrations.md Normal file
View File

@ -0,0 +1,20 @@
## AngularJS
ShowdownJS project provides seamless integration with AngularJS via a plugin.
Check [`ng-showdown`](https://github.com/showdownjs/ngShowdown) repository for more information.
## TypeScript
If you're using TypeScript, you may want to use the types from the [DefinitelyTyped][definitely-typed] repository.
## SystemJS/JSPM
To integrate ShowdownJS with SystemJS, you can use a third-party [system-md plugin](https://github.com/guybedford/system-md).
## Vue.js
To use ShowdownJS as a Vue component, you can check [vue-showdown](https://vue-showdown.js.org/).
[definitely-typed]: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/showdown

View File

@ -94,6 +94,11 @@ Once installed, you can use Showndown according to the chosen method:
<h1 id="hellomarkdown">hello, markdown!</h1> <h1 id="hellomarkdown">hello, markdown!</h1>
``` ```
!!! warning "Potential XSS vulnerabilities"
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.
Please refer to the [Markdown's XSS vulnerability](xss.md) page for more information.
## Other installation methods ## Other installation methods
### Tarball ### Tarball

110
docs/xss.md Normal file
View File

@ -0,0 +1,110 @@
# Markdown's XSS vulnerability
## Introduction
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 users cookies and do other malicious actions (like steal credentials). As a countermeasure, you should always filter user input for suspicious content. Showdown doesnt include an XSS filter, so you must provide your own. But be careful in how you do it.
## Markdown is inherently unsafe
Markdown syntax allows the inclusion of arbitrary HTML. For example, below is a perfectly valid Markdown:
```md
This is a regular paragraph.
<table>
<tr><td>Foo</td></tr>
</table>
This is another regular paragraph.
```
This means that an attacker could do something like this:
```md
This is a regular paragraph.
<script>alert('xss');</script>
This is another regular paragraph.
```
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".
## Whitelist / blacklist can't prevent XSS
Consider the following Markdown content:
```md
hello <a href="www.google.com">*you*</a>
```
As you can see, it's a link, nothing malicious about this. And `<a>` tags are pretty innocuous, right? Showdown should definitely allow them. But what if the content is slightly altered, like this:
```md
hello <a name="n" href="javascript:alert('xss')">*you*</a>
```
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 `<a>` tags) of scripting attacks. In fact, a regular HTML XSS prevention library should catch this kind of straightforward attack.
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.
## Strip HTML tags is not enough
Consider the following Markdown input:
```md
[some text](javascript:alert('xss'))
```
Showdown will correctly parse this piece of Markdown input as:
```html
<a href="javascript:alert('xss')">some text</a>
```
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.
## Mixed HTML/Markdown XSS attack
Consider the following piece of Markdown:
```md
> hello <a name="n"
> href="javascript:alert('xss')">*you*</a>
```
If you apply an XSS filter to filter bad HTML in this Markdown input, the XSS filter, expecting HTML, will likely think the `<a>` 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 `<a>` element and leave it alone. But when Markdown converts this to HTML, you get this:
```html
<blockquote>
<p>hello <a name="n"
href="javascript:alert('xss')"><em>you</em></a></p>
</blockquote>
```
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, youve got a link containing an XSS attack!
Did Markdown generate the HTML? No, the HTML was already in plain sight in the input. The XSS filter couldnt catch it because the input doesnt follow HTML rules: its a mix of Markdown and HTML, and the filter doesnt know a dime about Markdown.
## Mitigate XSS
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.
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.
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.
## Conclusion
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:
* **removing HTML entities from Markdown does not prevent XSS**. Markdown syntax can generate XSS attacks.
* **XSS filtering should be done after Showdown has processed input, not before or during**. If you filter before, it will break some of Markdowns features and will leave security holes.
* **perform the necessary filtering server-side, not client-side**. XSS filtering libraries are useful but should not be used blindly.
## Disclaimer
This page is based on the excellent article: ["Markdown and XSS"][1] by [Michel Fortin][2]
[1]: https://michelf.ca/blog/2010/markdown-and-xss/
[2]: https://github.com/michelf

80
karma.browserstack.js Normal file
View File

@ -0,0 +1,80 @@
module.exports = function (config) {
config.set({
// global config of your BrowserStack account
browserStack: {
username: process.env.BROWSERSTACK_USERNAME,
accessKey: process.env.BROWSERSTACK_ACCESSKEY,
project: process.env.BROWSERSTACK_PROJECT_NAME || 'showdown',
build: process.env.BROWSERSTACK_BUILD_NAME || require('./package.json').version,
name: process.env.COMMIT_MSG || 'Unit Testing'
},
// define browsers
customLaunchers: {
bstack_chrome_windows: {
base: 'BrowserStack',
browser: 'chrome',
browser_version: '49',
os: 'Windows',
os_version: '10'
},
bstack_firefox_windows: {
base: 'BrowserStack',
browser: 'firefox',
browser_version: '44',
os: 'Windows',
os_version: '10'
},
bstack_edge_windows: {
base: 'BrowserStack',
browser: 'edge',
browser_version: '15',
os: 'Windows',
os_version: '10'
},
bstack_ie11_windows: {
base: 'BrowserStack',
browser: 'ie',
browser_version: '11',
os: 'Windows',
os_version: '10'
},
bstack_macos_safari: {
base: 'BrowserStack',
browser: 'safari',
browser_version: '10.1',
os: 'OS X',
os_version: 'Sierra'
},
bstack_iphoneX: {
base: 'BrowserStack',
browser: 'safari',
os: 'ios',
os_version: '11.0',
device: 'iPhone X',
real_mobile: true
},
bstack_android: {
base: 'BrowserStack',
browser: 'chrome',
os: 'android',
os_version:'4.4',
device: 'Samsung Galaxy Tab 4',
realMobile: true
}
},
browsers: ['bstack_chrome_windows', 'bstack_firefox_windows', 'bstack_ie11_windows', 'bstack_edge_windows', 'bstack_iphoneX', 'bstack_macos_safari', 'bstack_android'],
frameworks: ['mocha', 'chai'],
reporters: ['dots', 'BrowserStack'],
files: [
{ pattern: '.build/showdown.js'},
{ pattern: 'src/options.js'},
// tests
{ pattern: 'test/unit/showdown*.js' }
//{ pattern: 'test/functional/showdown*.js' },
],
singleRun: true,
concurrency: Infinity
});
};

36
karma.conf.js Normal file
View File

@ -0,0 +1,36 @@
module.exports = function (config) {
config.set({
client: {
captureConsole: true
},
browserConsoleLogOptions: {
level: 'log',
format: '%b %T: %m',
terminal: true
},
logLevel: config.LOG_LOG,
frameworks: ['mocha', 'chai'],
files: [
{ pattern: '.build/showdown.js'},
{ pattern: 'src/options.js'},
// tests
{ pattern: 'test/unit/showdown*.js' },
{ pattern: 'test/functional/showdown*.js' },
],
reporters: ['progress'],
port: 9876, // karma web server port
colors: true,
browsers: ['ChromeHeadless', 'FirefoxHeadless', 'jsdom'],
autoWatch: false,
singleRun: true, // Karma captures browsers, runs the tests and exits
//concurrency: Infinity,
customLaunchers: {
'FirefoxHeadless': {
base: 'Firefox',
flags: [
'-headless',
]
}
},
});
};

View File

@ -32,4 +32,13 @@ nav:
- Donations: donations.md - Donations: donations.md
- Quickstart: - Quickstart:
- Quickstart: quickstart.md - Quickstart: quickstart.md
- Compatibility: compatibility.md - Compatibility: compatibility.md
- Configuration:
- Showdown options: configuration.md
- Available options: available-options.md
- Flavors: flavors.md
- CLI: cli.md
- Integrations: integrations.md
- Extensions:
- Overview: extensions.md
- Create an extension: create-extension.md

2266
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "showdown", "name": "showdown",
"version": "2.0.0", "version": "3.0.0-alpha",
"description": "A Markdown to HTML converter written in Javascript", "description": "A Markdown to HTML converter written in Javascript",
"author": "Estevão Santos", "author": "Estevão Santos",
"homepage": "http://showdownjs.com/", "homepage": "http://showdownjs.com/",
@ -58,16 +58,27 @@
"grunt-conventional-github-releaser": "^1.0.0", "grunt-conventional-github-releaser": "^1.0.0",
"grunt-endline": "^0.7.0", "grunt-endline": "^0.7.0",
"grunt-eslint": "^24.0.0", "grunt-eslint": "^24.0.0",
"grunt-mocha-test": "^0.13.3",
"grunt-simple-mocha": "^0.4.0", "grunt-simple-mocha": "^0.4.0",
"karma": "^6.3.17",
"karma-browserstack-launcher": "^1.6.0",
"karma-chai": "^0.1.0",
"karma-chrome-launcher": "^3.1.1",
"karma-firefox-launcher": "^2.1.2",
"karma-jsdom-launcher": "^12.0.0",
"karma-mocha": "^2.0.1",
"load-grunt-tasks": "^5.1.0", "load-grunt-tasks": "^5.1.0",
"performance-now": "^2.1.0", "performance-now": "^2.1.0",
"quiet-grunt": "^0.2.0", "quiet-grunt": "^0.2.0",
"semver-sort": "^1.0.0",
"sinon": "*", "sinon": "*",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21"
"semver-sort": "^1.0.0"
}, },
"dependencies": { "dependencies": {
"commander": "^9.0.0", "commander": "^9.0.0",
"jsdom": "^19.0.0" "jsdom": "^19.0.0"
},
"overrides": {
"minimist": "^1.2.6"
} }
} }

View File

@ -47,7 +47,15 @@ showdown.helper.isFunction = function (a) {
*/ */
showdown.helper.isArray = function (a) { showdown.helper.isArray = function (a) {
'use strict'; 'use strict';
return Array.isArray(a); let isArray;
if (!Array.isArray) {
isArray = function (arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
} else {
isArray = Array.isArray;
}
return isArray(a);
}; };
/** /**
@ -320,11 +328,48 @@ showdown.helper.splitAtIndex = function (str, index) {
return [str.substring(0, index), str.substring(index)]; return [str.substring(0, index), str.substring(index)];
}; };
/**
* MurmurHash3's mixing function
* https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript/47593316#47593316
*
* @param {string} string
* @returns {Number}
*/
/*jshint bitwise: false*/
function xmur3 (str) {
for (var i = 0, h = 1779033703 ^ str.length; i < str.length; i++) {
h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
h = h << 13 | h >>> 19;
}
return function () {
h = Math.imul(h ^ h >>> 16, 2246822507);
h = Math.imul(h ^ h >>> 13, 3266489909);
return (h ^= h >>> 16) >>> 0;
};
}
/**
* Random Number Generator
* https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript/47593316#47593316
*
* @param {Number} seed
* @returns {Number}
*/
/*jshint bitwise: false*/
function mulberry32 (a) {
return function () {
var t = a += 0x6D2B79F5;
t = Math.imul(t ^ t >>> 15, t | 1);
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
/** /**
* Obfuscate an e-mail address through the use of Character Entities, * Obfuscate an e-mail address through the use of Character Entities,
* transforming ASCII characters into their equivalent decimal or hex entities. * transforming ASCII characters into their equivalent decimal or hex entities.
* *
* Since it has a random component, subsequent calls to this function produce different results
* *
* @param {string} mail * @param {string} mail
* @returns {string} * @returns {string}
@ -343,12 +388,15 @@ showdown.helper.encodeEmailAddress = function (mail) {
} }
]; ];
// RNG seeded with mail, so that we can get determined results for each email.
var rand = mulberry32(xmur3(mail));
mail = mail.replace(/./g, function (ch) { mail = mail.replace(/./g, function (ch) {
if (ch === '@') { if (ch === '@') {
// this *must* be encoded. I insist. // this *must* be encoded. I insist.
ch = encode[Math.floor(Math.random() * 2)](ch); ch = encode[Math.floor(rand() * 2)](ch);
} else { } else {
var r = Math.random(); var r = rand();
// roughly 10% raw, 45% hex, 45% dec // roughly 10% raw, 45% hex, 45% dec
ch = ( ch = (
r > 0.9 ? encode[2](ch) : r > 0.45 ? encode[1](ch) : encode[0](ch) r > 0.9 ? encode[2](ch) : r > 0.45 ? encode[1](ch) : encode[0](ch)
@ -405,9 +453,9 @@ showdown.helper.repeat = function (str, count) {
/** /**
* String.prototype.padEnd polyfill * String.prototype.padEnd polyfill
* *
* @param str * @param {string} str
* @param targetLength * @param {int} targetLength
* @param padString * @param {string} [padString]
* @returns {string} * @returns {string}
*/ */
showdown.helper.padEnd = function padEnd (str, targetLength, padString) { showdown.helper.padEnd = function padEnd (str, targetLength, padString) {
@ -569,6 +617,26 @@ if (typeof (console) === 'undefined') {
}; };
} }
// Math.imul() polyfill
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul
if (!Math.imul) {
Math.imul = function (opA, opB) {
opB |= 0; // ensure that opB is an integer. opA will automatically be coerced.
// floating points give us 53 bits of precision to work with plus 1 sign bit
// automatically handled for our convienence:
// 1. 0x003fffff /*opA & 0x000fffff*/ * 0x7fffffff /*opB*/ = 0x1fffff7fc00001
// 0x1fffff7fc00001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/
var result = (opA & 0x003fffff) * opB;
// 2. We can remove an integer coersion from the statement above because:
// 0x1fffff7fc00001 + 0xffc00000 = 0x1fffffff800001
// 0x1fffffff800001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/
if (opA & 0xffc00000 /*!== 0*/) {
result += (opA & 0xffc00000) * opB | 0;
}
return result | 0;
};
}
/** /**
* Common regexes. * Common regexes.
* We declare some common regexes to improve performance * We declare some common regexes to improve performance

View File

@ -34,12 +34,12 @@ showdown.subParser('makehtml.metadata', function (text, options, globals) {
}); });
} }
text = text.replace(/^\s*«««+(\S*?)\n([\s\S]+?)\n»»»+\n/, function (wholematch, format, content) { text = text.replace(/^\s*«««+\s*(\S*?)\n([\s\S]+?)\n»»»+\s*\n/, function (wholematch, format, content) {
parseMetadataContents(content); parseMetadataContents(content);
return '¨M'; return '¨M';
}); });
text = text.replace(/^\s*---+(\S*?)\n([\s\S]+?)\n---+\n/, function (wholematch, format, content) { text = text.replace(/^\s*---+\s*(\S*?)\n([\s\S]+?)\n---+\s*\n/, function (wholematch, format, content) {
if (format) { if (format) {
globals.metadata.format = format; globals.metadata.format = format;
} }

View File

@ -1,70 +1,161 @@
showdown.subParser('makeMarkdown.table', function (node, options, globals) { showdown.subParser('makeMarkdown.table',
'use strict'; /**
*
* @param {DocumentFragment} node
* @param {{}} options
* @param {{}} globals
* @returns {string}
*/
function (node, options, globals) {
'use strict';
var txt = '', var txt = '',
tableArray = [[], []], tableArray = [[], []],
headings = node.querySelectorAll('thead>tr>th'), headings,
rows = node.querySelectorAll('tbody>tr'), rows = [],
i, ii; colCount,
for (i = 0; i < headings.length; ++i) { i,
var headContent = showdown.subParser('makeMarkdown.tableCell')(headings[i], options, globals), ii;
allign = '---';
if (headings[i].hasAttribute('style')) { /**
var style = headings[i].getAttribute('style').toLowerCase().replace(/\s/g, ''); * @param {Element} tr
switch (style) { */
case 'text-align:left;': function iterateRow (tr) {
allign = ':---'; var children = tr.childNodes,
break; cols = [];
case 'text-align:right;': // we need to iterate by order, since td and th can be used interchangeably and in any order
allign = '---:'; // we will ignore malformed stuff, comments and floating text.
break; for (var i = 0; i < children.length; ++i) {
case 'text-align:center;': var childName = children[i].nodeName.toUpperCase();
allign = ':---:'; if (childName === 'TD' || childName === 'TH') {
break; cols.push(children[i]);
}
}
tableArray[0][i] = headContent.trim();
tableArray[1][i] = allign;
}
for (i = 0; i < rows.length; ++i) {
var r = tableArray.push([]) - 1,
cols = rows[i].getElementsByTagName('td');
for (ii = 0; ii < headings.length; ++ii) {
var cellContent = ' ';
if (typeof cols[ii] !== 'undefined') {
cellContent = showdown.subParser('makeMarkdown.tableCell')(cols[ii], options, globals);
}
tableArray[r].push(cellContent);
}
}
var cellSpacesCount = 3;
for (i = 0; i < tableArray.length; ++i) {
for (ii = 0; ii < tableArray[i].length; ++ii) {
var strLen = tableArray[i][ii].length;
if (strLen > cellSpacesCount) {
cellSpacesCount = strLen;
}
}
}
for (i = 0; i < tableArray.length; ++i) {
for (ii = 0; ii < tableArray[i].length; ++ii) {
if (i === 1) {
if (tableArray[i][ii].slice(-1) === ':') {
tableArray[i][ii] = showdown.helper.padEnd(tableArray[i][ii].slice(0, -1), cellSpacesCount - 1, '-') + ':';
} else {
tableArray[i][ii] = showdown.helper.padEnd(tableArray[i][ii], cellSpacesCount, '-');
} }
} else { }
tableArray[i][ii] = showdown.helper.padEnd(tableArray[i][ii], cellSpacesCount); return cols;
}
// first lets look for <thead>
// we will ignore thead without <tr> children
// also, since markdown doesn't support tables with multiple heading rows, only the first one will be transformed
// the rest will count as regular rows
if (node.querySelectorAll(':scope>thead').length !== 0 && node.querySelectorAll(':scope>thead>tr').length !== 0) {
var thead = node.querySelectorAll(':scope>thead>tr');
// thead>tr can have td and th children
for (i = 0; i < thead.length; ++i) {
rows.push(iterateRow(thead[i]));
} }
} }
txt += '| ' + tableArray[i].join(' | ') + ' |\n';
}
return txt.trim(); // now let's look for tbody
}); // we will ignore tbody without <tr> children
if (node.querySelectorAll(':scope>tbody').length !== 0 && node.querySelectorAll(':scope>tbody>tr').length !== 0) {
var tbody = node.querySelectorAll(':scope>tbody>tr');
// tbody>tr can have td and th children, although th are not very screen reader friendly
for (i = 0; i < tbody.length; ++i) {
rows.push(iterateRow(tbody[i]));
}
}
// now look for tfoot
if (node.querySelectorAll(':scope>tfoot').length !== 0 && node.querySelectorAll(':scope>tfoot>tr').length !== 0) {
var tfoot = node.querySelectorAll(':scope>tfoot>tr');
// tfoot>tr can have td and th children, although th are not very screen reader friendly
for (i = 0; i < tfoot.length; ++i) {
rows.push(iterateRow(tfoot[i]));
}
}
// lastly look for naked tr
if (node.querySelectorAll(':scope>tr').length !== 0) {
var tr = node.querySelectorAll(':scope>tr');
// tfoot>tr can have td and th children, although th are not very screen reader friendly
for (i = 0; i < tr.length; ++i) {
rows.push(iterateRow(tr[i]));
}
}
// TODO: implement <caption> in tables https://developer.mozilla.org/pt-BR/docs/Web/HTML/Element/caption
// note: <colgroup> is ignored, since they are basically styling
// we need now to account for cases of completely empty tables, like <table></table> or equivalent
if (rows.length === 0) {
// table is empty, return empty text
return txt;
}
// count the first row. We need it to trim the table (if table rows have inconsistent number of columns)
colCount = rows[0].length;
// let's shift the first row as a heading
headings = rows.shift();
for (i = 0; i < headings.length; ++i) {
var headContent = showdown.subParser('makeMarkdown.tableCell')(headings[i], globals),
align = '---';
if (headings[i].hasAttribute('style')) {
var style = headings[i].getAttribute('style').toLowerCase().replace(/\s/g, '');
switch (style) {
case 'text-align:left;':
align = ':---';
break;
case 'text-align:right;':
align = '---:';
break;
case 'text-align:center;':
align = ':---:';
break;
}
}
tableArray[0][i] = headContent.trim();
tableArray[1][i] = align;
}
// now iterate through the rows and create the pseudo output (not pretty yet)
for (i = 0; i < rows.length; ++i) {
var r = tableArray.push([]) - 1;
for (ii = 0; ii < colCount; ++ii) {
var cellContent = ' ';
if (typeof rows[i][ii] !== 'undefined') {
// Note: if rows[i][ii] is undefined, it means the row has fewer elements than the header,
// and empty content will be added
cellContent = showdown.subParser('makeMarkdown.tableCell')(rows[i][ii], globals);
}
tableArray[r].push(cellContent);
}
}
// now tidy up the output, aligning cells and stuff
var cellSpacesCount = 3;
for (i = 0; i < tableArray.length; ++i) {
for (ii = 0; ii < tableArray[i].length; ++ii) {
var strLen = tableArray[i][ii].length;
if (strLen > cellSpacesCount) {
cellSpacesCount = strLen;
}
}
}
for (i = 0; i < tableArray.length; ++i) {
for (ii = 0; ii < tableArray[i].length; ++ii) {
if (i === 1) {
if (tableArray[i][ii].slice(-1) === ':') {
tableArray[i][ii] = showdown.helper.padEnd(tableArray[i][ii].slice(0, -1), cellSpacesCount - 1, '-') + ':';
} else {
tableArray[i][ii] = showdown.helper.padEnd(tableArray[i][ii], cellSpacesCount, '-');
}
} else {
tableArray[i][ii] = showdown.helper.padEnd(tableArray[i][ii], cellSpacesCount);
}
}
txt += '| ' + tableArray[i].join(' | ') + ' |\n';
}
return txt.trim();
}
);

6
test/bootstrap.js vendored Normal file
View File

@ -0,0 +1,6 @@
//.webstorm.bootstrap.js
var chai = require('chai');
global.chai = chai;
global.expect = chai.expect;
global.showdown = require('../.build/showdown.js');
global.getDefaultOpts = require('./optionswp.js').getDefaultOpts;

View File

@ -0,0 +1,8 @@
<p><strong>some</strong> markdown text</p>
<ul>
<li>a list</li>
<li>another list ---</li>
<li>and stuff</li>
</ul>
<p>a paragraph --- with dashes</p>
<hr />

View File

@ -0,0 +1,16 @@
---
title: This is the document title
language: en
author: Tivie
---
**some** markdown text
- a list
- another list ---
- and stuff
a paragraph --- with dashes
---

View File

@ -8,7 +8,7 @@
require('source-map-support').install(); require('source-map-support').install();
require('chai').should(); require('chai').should();
var fs = require('fs'); let fs = require('fs');
function getTestSuite (dir) { function getTestSuite (dir) {
return fs.readdirSync(dir) return fs.readdirSync(dir)
@ -17,20 +17,20 @@
} }
function getJsonTestSuite (file) { function getJsonTestSuite (file) {
var json = JSON.parse(fs.readFileSync(file, 'utf8')); let json = JSON.parse(fs.readFileSync(file, 'utf8'));
return mapJson(json, file); return mapJson(json, file);
} }
function filter () { function filter () {
return function (file) { return function (file) {
var ext = file.slice(-3); let ext = file.slice(-3);
return (ext === '.md'); return (ext === '.md');
}; };
} }
function map (dir) { function map (dir) {
return function (file) { return function (file) {
var oFile = 'file://' + process.cwd().replace(/\\/g, '/') + dir + file, let oFile = 'file://' + process.cwd().replace(/\\/g, '/') + dir + file,
name = file.replace('.md', ''), name = file.replace('.md', ''),
htmlPath = dir + name + '.html', htmlPath = dir + name + '.html',
html = fs.readFileSync(htmlPath, 'utf8'), html = fs.readFileSync(htmlPath, 'utf8'),
@ -47,12 +47,12 @@
} }
function mapJson (jsonArray, file) { function mapJson (jsonArray, file) {
var tcObj = {}; let tcObj = {};
for (var i = 0; i < jsonArray.length; ++i) { for (let i = 0; i < jsonArray.length; ++i) {
var section = jsonArray[i].section; let section = jsonArray[i].section;
var name = jsonArray[i].section + '_' + jsonArray[i].example; let name = jsonArray[i].section + '_' + jsonArray[i].example;
var md = jsonArray[i].markdown; let md = jsonArray[i].markdown;
var html = jsonArray[i].html; let html = jsonArray[i].html;
if (!tcObj.hasOwnProperty(section)) { if (!tcObj.hasOwnProperty(section)) {
tcObj[section] = []; tcObj[section] = [];
} }

View File

@ -100,6 +100,8 @@ describe('makeHtml() features testsuite', function () {
converter = new showdown.Converter({openLinksInNewWindow: true}); converter = new showdown.Converter({openLinksInNewWindow: true});
} else if (testsuite[i].name === '#355.simplifiedAutoLink-URLs-inside-parenthesis-followed-by-another-character-are-not-parsed-correctly') { } else if (testsuite[i].name === '#355.simplifiedAutoLink-URLs-inside-parenthesis-followed-by-another-character-are-not-parsed-correctly') {
converter = new showdown.Converter({simplifiedAutoLink: true}); converter = new showdown.Converter({simplifiedAutoLink: true});
} else if (testsuite[i].name === '#709.allow-whitespaces-after-end-in-metadata') {
converter = new showdown.Converter({metadata: true});
} else if (testsuite[i].name === 'relativePathBaseUrl') { } else if (testsuite[i].name === 'relativePathBaseUrl') {
converter = new showdown.Converter({relativePathBaseUrl: 'http://my.site.com/'}); converter = new showdown.Converter({relativePathBaseUrl: 'http://my.site.com/'});
} else { } else {
@ -199,7 +201,7 @@ describe('makeHtml() features testsuite', function () {
function testImageUrlExists (imgUrl) { function testImageUrlExists (imgUrl) {
// Strip the quotes // Strip the quotes
imgUrl = imgUrl.substr(0, imgUrl.length - 1).substr(1); imgUrl = imgUrl.slice(1, -1);
return function (done) { return function (done) {
(imgUrl.startsWith('http://') ? http : https).get(imgUrl, function (res) { (imgUrl.startsWith('http://') ? http : https).get(imgUrl, function (res) {
expect(res.statusCode).to.equal(200); expect(res.statusCode).to.equal(200);

View File

@ -0,0 +1,39 @@
<table>
<caption>some table</caption>
<colgroup>
<col span="2" style="background-color:red">
<col style="background-color:yellow">
</colgroup>
<thead>
<tr>
<th scope="col">head 1</th>
<th scope="col">head 2</th>
<th scope="col">head 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>row 1: col 1</td>
<td>row 1: col 2</td>
<td>row 1: col 3</td>
</tr>
<tr>
<td>row 2: col 1</td>
<td>row 2: col 2</td>
<td>row 2: col 3</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>footer 1</td>
<td>footer 2</td>
<td>footer 3</td>
</tr>
<tr>
<td>footer 4</td>
<td>footer 5</td>
<td>footer 6</td>
</tr>
</tfoot>
</table>

View File

@ -0,0 +1,6 @@
| head 1 | head 2 | head 3 |
| ------------ | ------------ | ------------ |
| row 1: col 1 | row 1: col 2 | row 1: col 3 |
| row 2: col 1 | row 2: col 2 | row 2: col 3 |
| footer 1 | footer 2 | footer 3 |
| footer 4 | footer 5 | footer 6 |

View File

@ -0,0 +1,8 @@
<table>
<thead>
<tr>
<th>foo</th>
<th>bar</th>
</tr>
</thead>
</table>

View File

@ -0,0 +1,2 @@
| foo | bar |
| --- | --- |

View File

@ -0,0 +1,6 @@
<table></table>
<table>
<caption>some stuff</caption>
</table>

View File

@ -0,0 +1,18 @@
<table>
<thead>
<tr>
<td></td>
<td></td>
</tr>
</thead>
<tbody>
<tr>
<td>foo</td>
<td>bar</td>
</tr>
<tr>
<td>barista</td>
<td>yes</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,4 @@
| | |
| ------- | ------- |
| foo | bar |
| barista | yes |

View File

@ -0,0 +1,17 @@
<table>
<tr>
<td>head 1</td>
<td>head 2</td>
<td>head 3</td>
</tr>
<tr>
<td>row 1: col 1</td>
<td>row 1: col 2</td>
<td>row 1: col 3</td>
</tr>
<tr>
<td>row 2: col 1</td>
<td>row 2: col 2</td>
<td>row 2: col 3</td>
</tr>
</table>

View File

@ -0,0 +1,4 @@
| head 1 | head 2 | head 3 |
| ------------ | ------------ | ------------ |
| row 1: col 1 | row 1: col 2 | row 1: col 3 |
| row 2: col 1 | row 2: col 2 | row 2: col 3 |

View File

@ -62,10 +62,11 @@ describe('showdown cli', function () {
describe('-v', function () { describe('-v', function () {
it('should display version', function () { it('should display version', function () {
var proc = spawnCLI(null, ['-V'], {}); let proc = spawnCLI(null, ['-V'], {}),
verRegex = /^(\d{1,2}\.\d{1,3}\.\d{1,3}(?:-(alpha)|(beta)|(rc-\d{1,2})))?/;
proc.status.should.equal(0); proc.status.should.equal(0);
proc.stdout.should.match(/^\d{1,2}\.\d{1,3}\.\d{1,3}/); proc.stdout.should.match(verRegex);
proc.stdout.should.match(/^(\d{1,2}\.\d{1,3}\.\d{1,3})/).and.capture(0).equals(packageJson.version); proc.stdout.should.match(verRegex).and.capture(0).equals(packageJson.version);
proc.stderr.should.equal(''); proc.stderr.should.equal('');
}); });
}); });

View File

@ -1,16 +1,15 @@
/** /**
* Created by Estevao on 31-05-2015. * Created by Estevao on 31-05-2015.
*/ */
require('source-map-support').install(); //let showdown = require('../../.build/showdown.js') || require('showdown');
require('chai').should(); chai.should();
require('sinon');
var showdown = require('../../.build/showdown.js');
describe('showdown.Converter', function () { describe('showdown.Converter', function () {
'use strict'; 'use strict';
describe('option methods', function () { describe('option methods', function () {
var converter = new showdown.Converter(); let converter = new showdown.Converter();
it('setOption() should set option foo=baz', function () { it('setOption() should set option foo=baz', function () {
converter.setOption('foo', 'baz'); converter.setOption('foo', 'baz');
@ -21,14 +20,14 @@ describe('showdown.Converter', function () {
}); });
it('getOptions() should contain foo=baz', function () { it('getOptions() should contain foo=baz', function () {
var options = converter.getOptions(); let options = converter.getOptions();
options.should.have.ownProperty('foo'); options.should.have.ownProperty('foo');
options.foo.should.equal('baz'); options.foo.should.equal('baz');
}); });
}); });
describe('metadata methods', function () { describe('metadata methods', function () {
var converter = new showdown.Converter(); let converter = new showdown.Converter();
it('_setMetadataPair() should set foo to bar', function () { it('_setMetadataPair() should set foo to bar', function () {
converter._setMetadataPair('foo', 'bar'); converter._setMetadataPair('foo', 'bar');
@ -47,18 +46,18 @@ describe('showdown.Converter', function () {
* Test setFlavor('github') * Test setFlavor('github')
*/ */
describe('github', function () { describe('github', function () {
var converter = new showdown.Converter(), let converter = new showdown.Converter(),
ghOpts = showdown.getFlavorOptions('github'); ghOpts = showdown.getFlavorOptions('github');
converter.setFlavor('github'); converter.setFlavor('github');
for (var opt in ghOpts) { for (let opt in ghOpts) {
if (ghOpts.hasOwnProperty(opt)) { if (ghOpts.hasOwnProperty(opt)) {
check(opt, ghOpts[opt]); check(opt, ghOpts[opt]);
} }
} }
function check (key, val) { function check (key, val) {
it('should set ' + opt + ' to ' + val, function () { it('should set ' + key + ' to ' + val, function () {
converter.getOption(key).should.equal(val); converter.getOption(key).should.equal(val);
}); });
} }
@ -72,19 +71,19 @@ describe('showdown.Converter', function () {
describe('flavor', function () { describe('flavor', function () {
it('should be vanilla by default', function () { it('should be vanilla by default', function () {
var converter = new showdown.Converter(); let converter = new showdown.Converter();
converter.getFlavor().should.equal('vanilla'); converter.getFlavor().should.equal('vanilla');
}); });
it('should be changed if global option is changed', function () { it('should be changed if global option is changed', function () {
showdown.setFlavor('github'); showdown.setFlavor('github');
var converter = new showdown.Converter(); let converter = new showdown.Converter();
converter.getFlavor().should.equal('github'); converter.getFlavor().should.equal('github');
showdown.setFlavor('vanilla'); showdown.setFlavor('vanilla');
}); });
it('should not be changed if converter is initialized before global change', function () { it('should not be changed if converter is initialized before global change', function () {
var converter = new showdown.Converter(); let converter = new showdown.Converter();
showdown.setFlavor('github'); showdown.setFlavor('github');
converter.getFlavor().should.equal('vanilla'); converter.getFlavor().should.equal('vanilla');
showdown.setFlavor('vanilla'); showdown.setFlavor('vanilla');
@ -93,7 +92,7 @@ describe('showdown.Converter', function () {
}); });
describe('extension methods', function () { describe('extension methods', function () {
var extObjMock = { let extObjMock = {
type: 'lang', type: 'lang',
filter: function () {} filter: function () {}
}, },
@ -102,13 +101,13 @@ describe('showdown.Converter', function () {
}; };
it('addExtension() should add an extension Object', function () { it('addExtension() should add an extension Object', function () {
var converter = new showdown.Converter(); let converter = new showdown.Converter();
converter.addExtension(extObjMock); converter.addExtension(extObjMock);
converter.getAllExtensions().language.should.contain(extObjMock); converter.getAllExtensions().language.should.contain(extObjMock);
}); });
it('addExtension() should unwrap an extension wrapped in a function', function () { it('addExtension() should unwrap an extension wrapped in a function', function () {
var converter = new showdown.Converter(); let converter = new showdown.Converter();
converter.addExtension(extObjFunc); converter.addExtension(extObjFunc);
converter.getAllExtensions().language.should.contain(extObjMock); converter.getAllExtensions().language.should.contain(extObjMock);
@ -116,7 +115,7 @@ describe('showdown.Converter', function () {
it('useExtension() should use a previous registered extension in showdown', function () { it('useExtension() should use a previous registered extension in showdown', function () {
showdown.extension('foo', extObjMock); showdown.extension('foo', extObjMock);
var converter = new showdown.Converter(); let converter = new showdown.Converter();
converter.useExtension('foo'); converter.useExtension('foo');
converter.getAllExtensions().language.should.contain(extObjMock); converter.getAllExtensions().language.should.contain(extObjMock);
@ -124,7 +123,7 @@ describe('showdown.Converter', function () {
}); });
it('removeExtension() should remove an added extension', function () { it('removeExtension() should remove an added extension', function () {
var converter = new showdown.Converter(); let converter = new showdown.Converter();
converter.addExtension(extObjMock); converter.addExtension(extObjMock);
converter.removeExtension(extObjMock); converter.removeExtension(extObjMock);
@ -133,7 +132,7 @@ describe('showdown.Converter', function () {
}); });
describe('events', function () { describe('events', function () {
var events = [ let events = [
'makehtml.anchors', 'makehtml.anchors',
'makehtml.autoLinks', 'makehtml.autoLinks',
'makehtml.blockGamut', 'makehtml.blockGamut',
@ -151,17 +150,17 @@ describe('showdown.Converter', function () {
//'tables' //'tables'
]; ];
for (var i = 0; i < events.length; ++i) { for (let i = 0; i < events.length; ++i) {
runListener(events[i] + '.before'); runListener(events[i] + '.before');
runListener(events[i] + '.after'); runListener(events[i] + '.after');
} }
function runListener (name) { function runListener (name) {
it('should listen to ' + name, function () { it('should listen to ' + name, function () {
var converter = new showdown.Converter(); let converter = new showdown.Converter();
converter.listen(name, function (event) { converter.listen(name, function (event) {
var evtName = event.getName(); let evtName = event.getName();
var text = event.getCapturedText(); let text = event.getCapturedText();
evtName.should.equal(name.toLowerCase()); evtName.should.equal(name.toLowerCase());
text.should.match(/^[\s\S]*foo[\s\S]*$/); text.should.match(/^[\s\S]*foo[\s\S]*$/);
return text; return text;

View File

@ -1,16 +1,15 @@
/** /**
* Created by Estevao on 15-01-2015. * Created by Tivie on 15-01-2015.
*/ */
require('source-map-support').install(); //let showdown = require('../../.build/showdown.js') || require('showdown');
require('chai').should(); chai.should();
require('sinon');
var showdown = require('../../.build/showdown.js');
describe('showdown.Converter', function () { describe('showdown.Converter', function () {
'use strict'; 'use strict';
describe('Converter.options extensions', function () { describe('Converter.options extensions', function () {
var runCount; let runCount;
showdown.extension('testext', function () { showdown.extension('testext', function () {
return [{ return [{
type: 'output', type: 'output',
@ -21,7 +20,7 @@ describe('showdown.Converter', function () {
}]; }];
}); });
var converter = new showdown.Converter({extensions: ['testext']}); let converter = new showdown.Converter({extensions: ['testext']});
it('output extensions should run once', function () { it('output extensions should run once', function () {
runCount = 0; runCount = 0;
@ -31,36 +30,36 @@ describe('showdown.Converter', function () {
}); });
describe('makeHtml() with option omitExtraWLInCodeBlocks', function () { describe('makeHtml() with option omitExtraWLInCodeBlocks', function () {
var converter = new showdown.Converter({omitExtraWLInCodeBlocks: true}), let converter = new showdown.Converter({omitExtraWLInCodeBlocks: true}),
text = 'var foo = bar;', text = 'var foo = bar;',
html = converter.makeHtml(' ' + text); html = converter.makeHtml(' ' + text);
it('should omit extra line after code tag', function () { it('should omit extra line after code tag', function () {
var expectedHtml = '<pre><code>' + text + '</code></pre>'; let expectedHtml = '<pre><code>' + text + '</code></pre>';
html.should.equal(expectedHtml); html.should.equal(expectedHtml);
}); });
}); });
describe('makeHtml() with option prefixHeaderId', function () { describe('makeHtml() with option prefixHeaderId', function () {
var converter = new showdown.Converter(), let converter = new showdown.Converter(),
text = 'foo header'; text = 'foo header';
it('should prefix header id with "section"', function () { it('should prefix header id with "section"', function () {
converter.setOption('prefixHeaderId', true); converter.setOption('prefixHeaderId', true);
var html = converter.makeHtml('# ' + text), let html = converter.makeHtml('# ' + text),
expectedHtml = '<h1 id="sectionfooheader">' + text + '</h1>'; expectedHtml = '<h1 id="sectionfooheader">' + text + '</h1>';
html.should.equal(expectedHtml); html.should.equal(expectedHtml);
}); });
it('should prefix header id with custom string', function () { it('should prefix header id with custom string', function () {
converter.setOption('prefixHeaderId', 'blabla'); converter.setOption('prefixHeaderId', 'blabla');
var html = converter.makeHtml('# ' + text), let html = converter.makeHtml('# ' + text),
expectedHtml = '<h1 id="blablafooheader">' + text + '</h1>'; expectedHtml = '<h1 id="blablafooheader">' + text + '</h1>';
html.should.equal(expectedHtml); html.should.equal(expectedHtml);
}); });
}); });
describe('makeHtml() with option metadata', function () { describe('makeHtml() with option metadata', function () {
var converter = new showdown.Converter(), let converter = new showdown.Converter(),
text1 = text1 =
'---SIMPLE\n' + '---SIMPLE\n' +
'foo: bar\n' + 'foo: bar\n' +
@ -75,7 +74,7 @@ describe('showdown.Converter', function () {
it('should correctly set metadata', function () { it('should correctly set metadata', function () {
converter.setOption('metadata', true); converter.setOption('metadata', true);
var expectedHtml = '', let expectedHtml = '',
expectedObj = {foo: 'bar', baz: 'bazinga'}, expectedObj = {foo: 'bar', baz: 'bazinga'},
expectedRaw = 'foo: bar\nbaz: bazinga', expectedRaw = 'foo: bar\nbaz: bazinga',
expectedFormat = 'SIMPLE'; expectedFormat = 'SIMPLE';
@ -87,7 +86,7 @@ describe('showdown.Converter', function () {
it('consecutive calls should reset metadata', function () { it('consecutive calls should reset metadata', function () {
converter.makeHtml(text2); converter.makeHtml(text2);
var expectedObj = {a: 'b', c: '123'}, let expectedObj = {a: 'b', c: '123'},
expectedRaw = 'a: b\nc: 123', expectedRaw = 'a: b\nc: 123',
expectedFormat = 'TIVIE'; expectedFormat = 'TIVIE';
converter.getMetadata().should.eql(expectedObj); converter.getMetadata().should.eql(expectedObj);

View File

@ -1,21 +1,19 @@
/** /**
* Created by Estevao on 15-01-2015. * Created by Estevao on 15-01-2015.
*/ */
require('source-map-support').install(); //let showdown = require('../../.build/showdown.js') || require('showdown');
require('chai').should(); chai.should();
require('sinon');
var showdown = require('../../.build/showdown.js');
describe('showdown.Converter', function () { describe('showdown.Converter', function () {
'use strict'; 'use strict';
describe('makeMarkdown()', function () { describe('makeMarkdown()', function () {
var converter = new showdown.Converter(); let converter = new showdown.Converter();
it('should parse a simple html string', function () { it('should parse a simple html string', function () {
var html = '<a href="/somefoo.html">a link</a>\n'; let html = '<a href="/somefoo.html">a link</a>\n';
var md = '[a link](</somefoo.html>)'; let md = '[a link](</somefoo.html>)';
converter.makeMarkdown(html).should.equal(md); converter.makeMarkdown(html).should.equal(md);
}); });

View File

@ -1,10 +1,7 @@
/** /**
* Created by Tivie on 27/01/2017. * Created by Tivie on 27/01/2017.
*/ */
require('source-map-support').install(); chai.should();
require('chai').should();
require('sinon');
var showdown = require('../../.build/showdown.js');
/*jshint expr: true*/ /*jshint expr: true*/
/*jshint -W053 */ /*jshint -W053 */
/*jshint -W010 */ /*jshint -W010 */
@ -12,16 +9,21 @@ var showdown = require('../../.build/showdown.js');
describe('encodeEmailAddress()', function () { describe('encodeEmailAddress()', function () {
'use strict'; 'use strict';
var encoder = showdown.helper.encodeEmailAddress, let encoder = showdown.helper.encodeEmailAddress,
email = 'foobar@example.com', email = 'foobar@example.com',
encodedEmail = encoder(email); encodedEmail = encoder(email),
encodedEmail2 = encoder(email);
it('should encode email', function () { it('should encode email', function () {
encodedEmail.should.not.equal(email); encodedEmail.should.not.equal(email);
}); });
it('should encode email determinated', function () {
encodedEmail.should.equal(encodedEmail2);
});
it('should decode to original email', function () { it('should decode to original email', function () {
var decodedEmail = encodedEmail.replace(/&#(.+?);/g, function (wm, cc) { let decodedEmail = encodedEmail.replace(/&#(.+?);/g, function (wm, cc) {
if (cc.charAt(0) === 'x') { if (cc.charAt(0) === 'x') {
//hex //hex
return String.fromCharCode('0' + cc); return String.fromCharCode('0' + cc);
@ -36,7 +38,7 @@ describe('encodeEmailAddress()', function () {
describe('isString()', function () { describe('isString()', function () {
'use strict'; 'use strict';
var isString = showdown.helper.isString; let isString = showdown.helper.isString;
it('should return true for new String Object', function () { it('should return true for new String Object', function () {
isString(new String('some string')).should.be.true; isString(new String('some string')).should.be.true;
@ -65,7 +67,7 @@ describe('isString()', function () {
describe('isFunction()', function () { describe('isFunction()', function () {
'use strict'; 'use strict';
var isFunction = showdown.helper.isFunction; let isFunction = showdown.helper.isFunction;
it('should return true for closures', function () { it('should return true for closures', function () {
isFunction(function () {}).should.be.true; isFunction(function () {}).should.be.true;
@ -76,8 +78,8 @@ describe('isFunction()', function () {
isFunction(foo).should.be.true; isFunction(foo).should.be.true;
}); });
it('should return true for function variables', function () { it('should return true for function letiables', function () {
var bar = function () {}; let bar = function () {};
isFunction(bar).should.be.true; isFunction(bar).should.be.true;
}); });
@ -96,14 +98,14 @@ describe('isFunction()', function () {
describe('isArray()', function () { describe('isArray()', function () {
'use strict'; 'use strict';
var isArray = showdown.helper.isArray; let isArray = showdown.helper.isArray;
it('should return true for short syntax arrays', function () { it('should return true for short syntax arrays', function () {
isArray([]).should.be.true; isArray([]).should.be.true;
}); });
it('should return true for array objects', function () { it('should return true for array objects', function () {
var myArr = new Array(); let myArr = new Array();
isArray(myArr).should.be.true; isArray(myArr).should.be.true;
}); });
@ -126,14 +128,14 @@ describe('isArray()', function () {
describe('isUndefined()', function () { describe('isUndefined()', function () {
'use strict'; 'use strict';
var isUndefined = showdown.helper.isUndefined; let isUndefined = showdown.helper.isUndefined;
it('should return true if nothing is passed', function () { it('should return true if nothing is passed', function () {
isUndefined().should.be.true; isUndefined().should.be.true;
}); });
it('should return true if a variable is initialized but not defined', function () { it('should return true if a letiable is initialized but not defined', function () {
var myVar; let myVar;
isUndefined(myVar).should.be.true; isUndefined(myVar).should.be.true;
}); });
@ -163,15 +165,15 @@ describe('isUndefined()', function () {
describe('stdExtName()', function () { describe('stdExtName()', function () {
'use strict'; 'use strict';
var stdExtName = showdown.helper.stdExtName; let stdExtName = showdown.helper.stdExtName;
it('should remove certain chars', function () { it('should remove certain chars', function () {
var str = 'bla_- \nbla'; let str = 'bla_- \nbla';
//[_?*+\/\\.^-] //[_?*+\/\\.^-]
stdExtName(str).should.not.match(/[_?*+\/\\.^-]/g); stdExtName(str).should.not.match(/[_?*+\/\\.^-]/g);
}); });
it('should make everything lowercase', function () { it('should make everything lowercase', function () {
var str = 'BLABLA'; let str = 'BLABLA';
//[_?*+\/\\.^-] //[_?*+\/\\.^-]
stdExtName(str).should.equal('blabla'); stdExtName(str).should.equal('blabla');
}); });
@ -179,7 +181,7 @@ describe('stdExtName()', function () {
describe('forEach()', function () { describe('forEach()', function () {
'use strict'; 'use strict';
var forEach = showdown.helper.forEach; let forEach = showdown.helper.forEach;
it('should throw an error if first parameter is undefined', function () { it('should throw an error if first parameter is undefined', function () {
(function () {forEach();}).should.throw('obj param is required'); (function () {forEach();}).should.throw('obj param is required');
@ -202,7 +204,7 @@ describe('forEach()', function () {
}); });
it('should iterate array items', function () { it('should iterate array items', function () {
var myArray = ['banana', 'orange', 'grape']; let myArray = ['banana', 'orange', 'grape'];
forEach(myArray, function (val, key, obj) { forEach(myArray, function (val, key, obj) {
key.should.be.a('number'); key.should.be.a('number');
(key % 1).should.equal(0); (key % 1).should.equal(0);
@ -212,7 +214,7 @@ describe('forEach()', function () {
}); });
it('should iterate over object properties', function () { it('should iterate over object properties', function () {
var myObj = {foo: 'banana', bar: 'orange', baz: 'grape'}; let myObj = {foo: 'banana', bar: 'orange', baz: 'grape'};
forEach(myObj, function (val, key, obj) { forEach(myObj, function (val, key, obj) {
myObj.should.have.ownProperty(key); myObj.should.have.ownProperty(key);
val.should.equal(myObj[key]); val.should.equal(myObj[key]);
@ -221,7 +223,7 @@ describe('forEach()', function () {
}); });
it('should iterate only over object own properties', function () { it('should iterate only over object own properties', function () {
var Obj1 = {foo: 'banana'}, let Obj1 = {foo: 'banana'},
myObj = Object.create(Obj1); myObj = Object.create(Obj1);
myObj.bar = 'orange'; myObj.bar = 'orange';
myObj.baz = 'grape'; myObj.baz = 'grape';
@ -239,10 +241,10 @@ describe('forEach()', function () {
describe('matchRecursiveRegExp()', function () { describe('matchRecursiveRegExp()', function () {
'use strict'; 'use strict';
var rRegExp = showdown.helper.matchRecursiveRegExp; let rRegExp = showdown.helper.matchRecursiveRegExp;
it('should match nested elements', function () { it('should match nested elements', function () {
var result = rRegExp('<div><div>a</div></div>', '<div\\b[^>]*>', '</div>', 'gim'); let result = rRegExp('<div><div>a</div></div>', '<div\\b[^>]*>', '</div>', 'gim');
result.should.deep.equal([['<div><div>a</div></div>', '<div>a</div>', '<div>', '</div>']]); result.should.deep.equal([['<div><div>a</div></div>', '<div>a</div>', '<div>', '</div>']]);
}); });
@ -251,9 +253,11 @@ describe('matchRecursiveRegExp()', function () {
describe('repeat()', function () { describe('repeat()', function () {
'use strict'; 'use strict';
it('work produce the same output as String.prototype.repeat()', function () { it('work produce the same output as String.prototype.repeat()', function () {
var str = 'foo', if (typeof String.prototype.repeat !== 'undefined') {
expected = str.repeat(100), let str = 'foo',
actual = showdown.helper.repeat(str, 100); expected = str.repeat(100),
expected.should.equal(actual); actual = showdown.helper.repeat(str, 100);
expected.should.equal(actual);
}
}); });
}); });

View File

@ -1,9 +1,7 @@
require('source-map-support').install(); /**
require('chai').should(); * Created by Tivie on 27/01/2017.
require('sinon'); */
var expect = require('chai').expect, //let showdown = require('../../.build/showdown.js') || require('showdown');
showdown = require('../../.build/showdown.js');
describe('showdown.options', function () { describe('showdown.options', function () {
'use strict'; 'use strict';
@ -19,7 +17,7 @@ describe('showdown.options', function () {
describe('getDefaultOptions()', function () { describe('getDefaultOptions()', function () {
it('should get default options', function () { it('should get default options', function () {
var opts = require('./optionswp.js').getDefaultOpts(true); let opts = getDefaultOpts(true);
expect(showdown.getDefaultOptions()).to.be.eql(opts); expect(showdown.getDefaultOptions()).to.be.eql(opts);
}); });
}); });
@ -28,7 +26,7 @@ describe('showdown.options', function () {
describe('showdown.extension()', function () { describe('showdown.extension()', function () {
'use strict'; 'use strict';
var extObjMock = { let extObjMock = {
type: 'lang', type: 'lang',
filter: function () {} filter: function () {}
}, },
@ -36,6 +34,8 @@ describe('showdown.extension()', function () {
return extObjMock; return extObjMock;
}; };
/*
// very flimsy test
describe('file loading', function () { describe('file loading', function () {
beforeEach(function () { beforeEach(function () {
@ -52,7 +52,7 @@ describe('showdown.extension()', function () {
}); });
}); });
*/
describe('objects', function () { describe('objects', function () {
it('should register an extension object', function () { it('should register an extension object', function () {
@ -77,14 +77,14 @@ describe('showdown.extension()', function () {
}); });
it('should refuse to register a generic object', function () { it('should refuse to register a generic object', function () {
var fn = function () { let fn = function () {
showdown.extension('foo', {}); showdown.extension('foo', {});
}; };
expect(fn).to.throw(); expect(fn).to.throw();
}); });
it('should refuse to register an extension with invalid type', function () { it('should refuse to register an extension with invalid type', function () {
var fn = function () { let fn = function () {
showdown.extension('foo', { showdown.extension('foo', {
type: 'foo' type: 'foo'
}); });
@ -93,7 +93,7 @@ describe('showdown.extension()', function () {
}); });
it('should refuse to register an extension without regex or filter', function () { it('should refuse to register an extension without regex or filter', function () {
var fn = function () { let fn = function () {
showdown.extension('foo', { showdown.extension('foo', {
type: 'lang' type: 'lang'
}); });
@ -102,7 +102,7 @@ describe('showdown.extension()', function () {
}); });
it('should refuse to register a listener extension without a listeners property', function () { it('should refuse to register a listener extension without a listeners property', function () {
var fn = function () { let fn = function () {
showdown.extension('foo', { showdown.extension('foo', {
type: 'listener' type: 'listener'
}); });
@ -120,7 +120,7 @@ describe('showdown.extension()', function () {
describe('showdown.getAllExtensions()', function () { describe('showdown.getAllExtensions()', function () {
'use strict'; 'use strict';
var extObjMock = { let extObjMock = {
type: 'lang', type: 'lang',
filter: function () {} filter: function () {}
}; };
@ -141,9 +141,9 @@ describe('showdown.setFlavor()', function () {
it('should set options correctly', function () { it('should set options correctly', function () {
showdown.setFlavor('github'); showdown.setFlavor('github');
var ghOpts = showdown.getFlavorOptions('github'), let ghOpts = showdown.getFlavorOptions('github'),
shOpts = showdown.getOptions(); shOpts = showdown.getOptions();
for (var opt in ghOpts) { for (let opt in ghOpts) {
if (ghOpts.hasOwnProperty(opt)) { if (ghOpts.hasOwnProperty(opt)) {
shOpts.should.have.property(opt); shOpts.should.have.property(opt);
shOpts[opt].should.equal(ghOpts[opt]); shOpts[opt].should.equal(ghOpts[opt]);
@ -154,10 +154,10 @@ describe('showdown.setFlavor()', function () {
it('should switch between flavors correctly', function () { it('should switch between flavors correctly', function () {
showdown.setFlavor('github'); showdown.setFlavor('github');
var ghOpts = showdown.getFlavorOptions('github'), let ghOpts = showdown.getFlavorOptions('github'),
shOpts = showdown.getOptions(), shOpts = showdown.getOptions(),
dfOpts = showdown.getDefaultOptions(); dfOpts = showdown.getDefaultOptions();
for (var opt in dfOpts) { for (let opt in dfOpts) {
if (ghOpts.hasOwnProperty(opt)) { if (ghOpts.hasOwnProperty(opt)) {
shOpts[opt].should.equal(ghOpts[opt]); shOpts[opt].should.equal(ghOpts[opt]);
} else { } else {
@ -165,9 +165,9 @@ describe('showdown.setFlavor()', function () {
} }
} }
showdown.setFlavor('original'); showdown.setFlavor('original');
var orOpts = showdown.getFlavorOptions('original'); let orOpts = showdown.getFlavorOptions('original');
shOpts = showdown.getOptions(); shOpts = showdown.getOptions();
for (opt in dfOpts) { for (let opt in dfOpts) {
if (orOpts.hasOwnProperty(opt)) { if (orOpts.hasOwnProperty(opt)) {
shOpts[opt].should.equal(orOpts[opt]); shOpts[opt].should.equal(orOpts[opt]);
} else { } else {