mirror of https://github.com/showdownjs/showdown
Compare commits
12 Commits
75b7707460
...
2d593d91f3
Author | SHA1 | Date |
---|---|---|
Estevão Soares dos Santos | 2d593d91f3 | |
Estevão Soares dos Santos | 9dab0826c6 | |
Estevão Soares dos Santos | 150c4c36cb | |
Estevão Soares dos Santos | d080d30011 | |
Estevão Soares dos Santos | 42959b78c1 | |
Barry Pollard | 4333646c8c | |
Antonio | e4c87cf5d5 | |
Antonio | fbbd16d261 | |
Antonio | 952c0184ca | |
Antonio | da09ee8565 | |
Antonio | b64f0bea9d | |
Antonio | 039c9be049 |
53
CREDITS.md
53
CREDITS.md
|
@ -1,53 +0,0 @@
|
|||
Credits
|
||||
=======
|
||||
|
||||
- Showdown v2
|
||||
* [Estevão Santos](https://github.com/tivie)
|
||||
* [SyntaxRules](https://github.com/SyntaxRules)
|
||||
|
||||
- Showdown v1
|
||||
* [Estevão Santos](https://github.com/tivie)
|
||||
* [Pascal Deschênes](https://github.com/pdeschen)
|
||||
|
||||
- Showdown v0
|
||||
* [Corey Innis](http://github.com/coreyti):<br/>
|
||||
Original GitHub project maintainer
|
||||
* [Remy Sharp](https://github.com/remy/):<br/>
|
||||
CommonJS-compatibility and more
|
||||
* [Konstantin Käfer](https://github.com/kkaefer/):<br/>
|
||||
CommonJS packaging
|
||||
* [Roger Braun](https://github.com/rogerbraun):<br/>
|
||||
Github-style code blocks
|
||||
* [Dominic Tarr](https://github.com/dominictarr):<br/>
|
||||
Documentation
|
||||
* [Cat Chen](https://github.com/CatChen):<br/>
|
||||
Export fix
|
||||
* [Titus Stone](https://github.com/tstone):<br/>
|
||||
Mocha tests, extension mechanism, and bug fixes
|
||||
* [Rob Sutherland](https://github.com/roberocity):<br/>
|
||||
The idea that lead to extensions
|
||||
* [Pavel Lang](https://github.com/langpavel):<br/>
|
||||
Code cleanup
|
||||
* [Ben Combee](https://github.com/unwiredben):<br/>
|
||||
Regex optimization
|
||||
* [Adam Backstrom](https://github.com/abackstrom):<br/>
|
||||
WebKit bugfix
|
||||
* [Pascal Deschênes](https://github.com/pdeschen):<br/>
|
||||
Grunt support, extension fixes + additions, packaging improvements, documentation
|
||||
* [Estevão Santos](https://github.com/tivie)<br/>
|
||||
Bug fixing and late maintainer
|
||||
* [Hannah Wolfe](https://github.com/ErisDS)<br/>
|
||||
Bug fixes
|
||||
* [Alexandre Courtiol](https://github.com/acourtiol)<br/>
|
||||
Bug fixes and build optimization
|
||||
* [Karthik Balakrishnan](https://github.com/torcellite)<br/>
|
||||
Support for table alignment
|
||||
* [rheber](https://github.com/rheber)<br/>
|
||||
Cli
|
||||
|
||||
|
||||
- Original Project
|
||||
* [John Gruber](http://daringfireball.net/projects/markdown/)<br/>
|
||||
Author of Markdown
|
||||
* [John Fraser](http://attacklab.net/)<br/>
|
||||
Author of Showdown
|
546
README.md
546
README.md
|
@ -2,541 +2,77 @@
|
|||
|
||||
![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)
|
||||
[![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)
|
||||
[![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)
|
||||
|
||||
------
|
||||
|
||||
Showdown is a JavaScript Markdown to HTML converter, based on the original works by John Gruber.
|
||||
Showdown can be used client side (in the browser) or server side (with Node.js).
|
||||
|
||||
## Live DEMO
|
||||
Showdown can be used on the client-side (in the browser) or server-side (with Node.js).
|
||||
|
||||
Check out a live demo here: http://demo.showdownjs.com/
|
||||
----
|
||||
|
||||
As you know, ShowdownJS is a free library and it will remain free forever. However, maintaining and improving the library costs time and money.
|
||||
## Live demo
|
||||
|
||||
If you like our work and find our library useful, please donate through [PayPal](https://www.paypal.me/tiviesantos)! Your contribution will be greatly appreciated and help me continue to develop this awesome library.
|
||||
|
||||
## License
|
||||
|
||||
ShowdownJS v 2.0 is released under the MIT license.
|
||||
Previous versions are released under BSD.
|
||||
<http://demo.showdownjs.com/>
|
||||
|
||||
## Who uses Showdown (or a fork)
|
||||
|
||||
- [GoogleCloudPlatform](https://github.com/GoogleCloudPlatform)
|
||||
- [Meteor](https://www.meteor.com/)
|
||||
- [Stackexchange](http://stackexchange.com/) - forked as [PageDown](https://code.google.com/p/pagedown/)
|
||||
- [docular](https://github.com/Vertafore/docular)
|
||||
- [md-page](https://github.com/oscarmorrison/md-page)
|
||||
- [QCObjects](https://qcobjects.dev)
|
||||
- [and some others...](https://www.npmjs.com/browse/depended/showdown)
|
||||
* [Antmarky](https://github.com/bandantonio/antmarky)
|
||||
* [GoogleCloudPlatform](https://github.com/GoogleCloudPlatform)
|
||||
* [Meteor](https://www.meteor.com/)
|
||||
* [StackExchange](http://stackexchange.com/) - forked as [PageDown](https://code.google.com/p/pagedown/)
|
||||
* [docular](https://github.com/Vertafore/docular)
|
||||
* [md-page](https://github.com/oscarmorrison/md-page)
|
||||
* [QCObjects](https://qcobjects.dev)
|
||||
* [and some others](https://www.npmjs.com/browse/depended/showdown)
|
||||
|
||||
## Installation
|
||||
## Quickstart
|
||||
|
||||
### Download tarball
|
||||
Check our documentation for the [quickstart guide][quickstart]
|
||||
and possible [installation methods][installation-methods].
|
||||
|
||||
You can download the latest release tarball directly from [releases][releases].
|
||||
## Showdown features
|
||||
|
||||
### Bower
|
||||
### Platform-agnostic
|
||||
|
||||
bower install showdown
|
||||
Showdown can easily be used on server-side and client-side.
|
||||
|
||||
### npm (server-side)
|
||||
### Two-way conversion
|
||||
|
||||
npm install showdown
|
||||
Showdown not only helps you to convert your Markdown documents into HTML but also can bring them back to Markdown from HTML.
|
||||
|
||||
### NuGet package
|
||||
### Extensions
|
||||
|
||||
PM> Install-Package showdownjs
|
||||
Expand Showdown's functionality using a powerful [extension system][extensions]. Extensions can also be used on server-side and client-side.
|
||||
|
||||
The NuGet Packages can be found [here](https://www.nuget.org/packages/showdownjs/).
|
||||
### CLI
|
||||
|
||||
### CDN
|
||||
Showdown comes bundled with a Command-line interface (CLI) tool that allows you to run Showdown converter from the command line.
|
||||
|
||||
You can also use one of several CDNs available:
|
||||
Check the [CLI][cli] section in the documentation for more information.
|
||||
|
||||
* jsDelivr
|
||||
## Contribution guide
|
||||
|
||||
https://cdn.jsdelivr.net/npm/showdown@<version tag>/dist/showdown.min.js
|
||||
|
||||
* cdnjs
|
||||
|
||||
https://cdnjs.cloudflare.com/ajax/libs/showdown/<version tag>/showdown.min.js
|
||||
|
||||
* unpkg
|
||||
|
||||
https://unpkg.com/showdown/dist/showdown.min.js
|
||||
|
||||
*Note*: replace `<version tag>` with an actual full length version you're interested in e.g. `1.9.0`
|
||||
## Browser Compatibility
|
||||
|
||||
Showdown has been tested successfully with:
|
||||
|
||||
* Firefox 1.5 and 2.0
|
||||
* Chrome 12.0
|
||||
* Internet Explorer 6 and 7
|
||||
* Safari 2.0.4
|
||||
* Opera 8.54 and 9.10
|
||||
* Netscape 8.1.2
|
||||
* Konqueror 3.5.4
|
||||
|
||||
In theory, Showdown will work in any browser that supports ECMA 262 3rd Edition (JavaScript 1.5).
|
||||
The converter itself might even work in things that aren't web browsers, like Acrobat. No promises.
|
||||
|
||||
|
||||
## Node compatibility
|
||||
|
||||
Showdown is intended to work on any supported Node.js version (see the [Node.js releases schedule](https://nodejs.org/en/about/releases/). The code may work with previous versions of Node.js, but no accomidations are made to ensure it does.
|
||||
|
||||
|
||||
## Legacy version
|
||||
|
||||
If you're looking for showdown v<1.0.0, you can find it in the [**legacy branch**][legacy-branch].
|
||||
|
||||
## Changelog
|
||||
|
||||
You can check the full [changelog][changelog]
|
||||
|
||||
## Extended documentation
|
||||
Check our [wiki pages][wiki] for examples and a more in-depth documentation.
|
||||
|
||||
|
||||
## Quick Example
|
||||
|
||||
### Node
|
||||
|
||||
```js
|
||||
var showdown = require('showdown'),
|
||||
converter = new showdown.Converter(),
|
||||
text = '# hello, markdown!',
|
||||
html = converter.makeHtml(text);
|
||||
```
|
||||
|
||||
### Browser
|
||||
|
||||
```js
|
||||
var converter = new showdown.Converter(),
|
||||
text = '# hello, markdown!',
|
||||
html = converter.makeHtml(text);
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
Both examples should output...
|
||||
|
||||
```html
|
||||
<h1 id="hellomarkdown">hello, markdown!</h1>
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
You can change some of showdown's default behavior through options.
|
||||
|
||||
### Setting options
|
||||
|
||||
Options can be set:
|
||||
|
||||
#### Globally
|
||||
|
||||
Setting a "global" option affects all instances of showdown
|
||||
|
||||
```js
|
||||
showdown.setOption('optionKey', 'value');
|
||||
```
|
||||
|
||||
#### Locally
|
||||
Setting a "local" option only affects the specified Converter object.
|
||||
Local options can be set:
|
||||
|
||||
* **through the constructor**
|
||||
```js
|
||||
var converter = new showdown.Converter({optionKey: 'value'});
|
||||
```
|
||||
|
||||
* **through the setOption() method**
|
||||
```js
|
||||
var converter = new showdown.Converter();
|
||||
converter.setOption('optionKey', 'value');
|
||||
```
|
||||
|
||||
### Getting an option
|
||||
|
||||
Showdown provides 2 methods (both local and global) to retrieve previous 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();
|
||||
```
|
||||
|
||||
### Retrieve the default options
|
||||
|
||||
You can get showdown's default options with:
|
||||
```js
|
||||
var defaultOptions = showdown.getDefaultOptions();
|
||||
```
|
||||
|
||||
### Valid Options
|
||||
|
||||
* **omitExtraWLInCodeBlocks**: (boolean) [default false] Omit the trailing newline in a code block. Ex:
|
||||
|
||||
This:
|
||||
```html
|
||||
<code><pre>var foo = 'bar';
|
||||
</pre></code>
|
||||
```
|
||||
Becomes this:
|
||||
```html
|
||||
<code><pre>var foo = 'bar';</pre></code>
|
||||
```
|
||||
|
||||
* **noHeaderId**: (boolean) [default false] Disable the automatic generation of header ids.
|
||||
Setting to true overrides **prefixHeaderId**
|
||||
|
||||
* **customizedHeaderId**: (boolean) [default false] Use text in curly braces as header id. **(since v1.7.0)**
|
||||
Example:
|
||||
```
|
||||
## Sample header {real-id} will use real-id as id
|
||||
```
|
||||
|
||||
* **ghCompatibleHeaderId**: (boolean) [default false] Generate header ids compatible with github style
|
||||
(spaces are replaced with dashes and a bunch of non alphanumeric chars are removed) **(since v1.5.5)**
|
||||
|
||||
* **prefixHeaderId**: (string/boolean) [default false] Add a prefix to the generated header ids.
|
||||
Passing a string will prefix that string to the header id. Setting to `true` will add a generic 'section' prefix.
|
||||
|
||||
* **rawPrefixHeaderId**: (boolean) [default false] Setting this option to true will prevent showdown from modifying the prefix.
|
||||
This might result in malformed IDs (if, for instance, the " char is used in the prefix).
|
||||
Has no effect if prefixHeaderId is set to false. **(since v 1.7.3)**
|
||||
|
||||
* **rawHeaderId**: (boolean) [default false] Remove only spaces, ' and " from generated header ids (including prefixes),
|
||||
replacing them with dashes (-). WARNING: This might result in malformed ids **(since v1.7.3)**
|
||||
|
||||
* **headerLevelStart**: (integer) [default 1] Set the header starting level. For instance, setting this to 3 means that
|
||||
|
||||
```md
|
||||
# foo
|
||||
```
|
||||
will be parsed as
|
||||
|
||||
```html
|
||||
<h3>foo</h3>
|
||||
```
|
||||
|
||||
* **parseImgDimensions**: (boolean) [default false] Enable support for setting image dimensions from within markdown syntax.
|
||||
Examples:
|
||||
```
|
||||
![foo](foo.jpg =100x80) simple, assumes units are in px
|
||||
![bar](bar.jpg =100x*) sets the height to "auto"
|
||||
![baz](baz.jpg =80%x5em) Image with width of 80% and height of 5em
|
||||
```
|
||||
|
||||
* **simplifiedAutoLink**: (boolean) [default false] Turning this option on will enable automatic linking to urls.
|
||||
This means that:
|
||||
|
||||
```md
|
||||
some text www.google.com
|
||||
```
|
||||
will be parsed as
|
||||
```html
|
||||
<p>some text <a href="www.google.com">www.google.com</a>
|
||||
```
|
||||
|
||||
* ~~**excludeTrailingPunctuationFromURLs**: (boolean) [default false] This option excludes trailing punctuation from autolinking urls.
|
||||
Punctuation excluded: `. ! ? ( )`. Only applies if **simplifiedAutoLink** option is set to `true`.~~
|
||||
|
||||
* **literalMidWordUnderscores**: (boolean) [default false] Turning this on will stop showdown from interpreting
|
||||
underscores in the middle of words as `<em>` and `<strong>` and instead treat them as literal underscores.
|
||||
|
||||
Example:
|
||||
|
||||
```md
|
||||
some text with__underscores__in middle
|
||||
```
|
||||
will be parsed as
|
||||
```html
|
||||
<p>some text with__underscores__in middle</p>
|
||||
```
|
||||
|
||||
* ~~**literalMidWordAsterisks**: (boolean) [default false] Turning this on will stop showdown from interpreting asterisks
|
||||
in the middle of words as `<em>` and `<strong>` and instead treat them as literal asterisks.~~
|
||||
|
||||
* **strikethrough**: (boolean) [default false] Enable support for strikethrough syntax.
|
||||
`~~strikethrough~~` as `<del>strikethrough</del>`
|
||||
|
||||
* **tables**: (boolean) [default false] Enable support for tables syntax. Example:
|
||||
|
||||
```md
|
||||
| h1 | h2 | h3 |
|
||||
|:------|:-------:|--------:|
|
||||
| 100 | [a][1] | ![b][2] |
|
||||
| *foo* | **bar** | ~~baz~~ |
|
||||
```
|
||||
|
||||
See the wiki for more info
|
||||
|
||||
* **tablesHeaderId**: (boolean) [default false] If enabled adds an id property to table headers tags.
|
||||
|
||||
* **ghCodeBlocks**: (boolean) [default true] Enable support for GFM code block style.
|
||||
|
||||
* **tasklists**: (boolean) [default false] Enable support for GFM tasklists. Example:
|
||||
|
||||
```md
|
||||
- [x] This task is done
|
||||
- [ ] This is still pending
|
||||
```
|
||||
* **smoothLivePreview**: (boolean) [default false] Prevents weird effects in live previews due to incomplete input
|
||||
|
||||
* **smartIndentationFix**: (boolean) [default false] Tries to smartly fix indentation problems related to es6 template
|
||||
strings in the midst of indented code.
|
||||
|
||||
* **disableForced4SpacesIndentedSublists**: (boolean) [default false] Disables the requirement of indenting sublists
|
||||
by 4 spaces for them to be nested, effectively reverting to the old behavior where 2 or 3 spaces were enough.
|
||||
**(since v1.5.0)**
|
||||
|
||||
* **simpleLineBreaks**: (boolean) [default false] Parses line breaks as `<br>`, without
|
||||
needing 2 spaces at the end of the line **(since v1.5.1)**
|
||||
|
||||
```md
|
||||
a line
|
||||
wrapped in two
|
||||
```
|
||||
|
||||
turns into:
|
||||
|
||||
```html
|
||||
<p>a line<br>
|
||||
wrapped in two</p>
|
||||
```
|
||||
|
||||
* **requireSpaceBeforeHeadingText**: (boolean) [default false] Makes adding a space between `#` and the header text mandatory **(since v1.5.3)**
|
||||
|
||||
* **ghMentions**: (boolean) [default false] Enables github @mentions, which link to the username mentioned **(since v1.6.0)**
|
||||
|
||||
* **ghMentionsLink**: (string) [default `https://github.com/{u}`] Changes the link generated by @mentions.
|
||||
Showdown will replace `{u}` with the username. Only applies if ghMentions option is enabled.
|
||||
Example: `@tivie` with ghMentionsOption set to `//mysite.com/{u}/profile` will result in `<a href="//mysite.com/tivie/profile">@tivie</a>`
|
||||
|
||||
* **encodeEmails**: (boolean) [default true] Enable e-mail addresses encoding through the use of Character Entities, transforming ASCII e-mail addresses into its equivalent decimal entities. (since v1.6.1)
|
||||
|
||||
NOTE: Prior to version 1.6.1, emails would always be obfuscated through dec and hex encoding.
|
||||
|
||||
* **openLinksInNewWindow**: (boolean) [default false] Open all links in new windows
|
||||
(by adding the attribute `target="_blank"` to `<a>` tags) **(since v1.7.0)**
|
||||
|
||||
* **backslashEscapesHTMLTags**: (boolean) [default false] Support for HTML Tag escaping. ex: `\<div>foo\</div>` **(since v1.7.2)**
|
||||
|
||||
* **emoji**: (boolean) [default false] Enable emoji support. Ex: `this is a :smile: emoji`
|
||||
For more info on available emojis, see https://github.com/showdownjs/showdown/wiki/Emojis **(since v.1.8.0)**
|
||||
|
||||
* **underline**: (boolean) [default false] ***EXPERIMENTAL FEATURE*** Enable support for underline.
|
||||
Syntax is **double** or **triple** **underscores** ex: `__underlined word__`. With this option enabled, underscores are no longer parses into `<em>` and `<strong>`.
|
||||
|
||||
* **ellipsis**: (boolean) [default true] Replaces three dots with the ellipsis unicode character.
|
||||
|
||||
* **completeHTMLDocument**: (boolean) [default false] Outputs a complete html document,
|
||||
including `<html>`, `<head>` and `<body>` tags' instead of an HTML fragment. (since v.1.8.5)
|
||||
|
||||
* **metadata**: (boolean) [default false] Enable support for document metadata (defined at the top of the document
|
||||
between `«««` and `»»»` or between `---` and `---`). (since v.1.8.5)
|
||||
|
||||
```js
|
||||
var conv = new showdown.Converter({metadata: true});
|
||||
var html = conv.makeHtml(someMd);
|
||||
var metadata = conv.getMetadata(); // returns an object with the document metadata
|
||||
```
|
||||
|
||||
* **splitAdjacentBlockquotes**: (boolean) [default false] Split adjacent blockquote blocks.(since v.1.8.6)
|
||||
|
||||
* **moreStyling**: (boolean) [default false] Adds some useful classes for css styling. (since v2.0.1)
|
||||
|
||||
- Tasklists: Adds the class `task-list-item-complete` to completed tasks items in GFM tasklists.
|
||||
|
||||
**NOTE**: Please note that until **version 1.6.0**, all of these options are ***DISABLED*** by default in the cli tool.
|
||||
|
||||
|
||||
## Flavors
|
||||
|
||||
You can also use flavors or presets to set the correct options automatically, so that 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 (as from v1.3.1)
|
||||
* github - GFM (GitHub Flavored Markdown)
|
||||
|
||||
|
||||
### Global
|
||||
```javascript
|
||||
showdown.setFlavor('github');
|
||||
```
|
||||
|
||||
### Instance
|
||||
```javascript
|
||||
converter.setFlavor('github');
|
||||
```
|
||||
|
||||
|
||||
## CLI Tool
|
||||
|
||||
Showdown also comes bundled with a Command Line Interface tool. You can check the [CLI wiki page][cli-wiki] for more info
|
||||
|
||||
## Integration with AngularJS
|
||||
|
||||
ShowdownJS project also provides seamlessly integration with AngularJS via a "plugin".
|
||||
Please visit https://github.com/showdownjs/ngShowdown for more information.
|
||||
|
||||
## Integration with TypeScript
|
||||
|
||||
If you're using TypeScript you maybe want to use the types from [DefinitelyTyped][definitely-typed]
|
||||
|
||||
## Integration with SystemJS/JSPM
|
||||
|
||||
Integration with SystemJS can be obtained via the third party ["system-md" plugin](https://github.com/guybedford/system-md).
|
||||
|
||||
## Integration with VueJS
|
||||
|
||||
To use ShowdownJS as a Vue component quickly, you can check [vue-showdown](https://vue-showdown.js.org/).
|
||||
|
||||
## XSS vulnerability
|
||||
|
||||
Showdown doesn't sanitize the input. This is by design since markdown relies on it to allow certain features to be correctly parsed into HTML.
|
||||
This, however, means XSS injection is quite possible.
|
||||
|
||||
Please refer to the wiki article [Markdown's XSS Vulnerability (and how to mitigate it)][xss-wiki]
|
||||
for more information.
|
||||
|
||||
## Extensions
|
||||
|
||||
Showdown allows additional functionality to be loaded via extensions. (you can find a list of known showdown extensions [here][ext-wiki])
|
||||
You can also find a boilerplate, to create your own extensions in [this repository][boilerplate-repo]
|
||||
|
||||
### Client-side Extension Usage
|
||||
|
||||
```js
|
||||
<script src="showdown.js" />
|
||||
<script src="twitter-extension.js" />
|
||||
|
||||
var converter = new showdown.Converter({ extensions: ['twitter'] });
|
||||
```
|
||||
|
||||
### Server-side Extension Usage
|
||||
|
||||
```js
|
||||
var showdown = require('showdown'),
|
||||
myExtension = require('myExtension'),
|
||||
converter = new showdown.Converter({ extensions: ['myExtension'] });
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
Building your clone of the repository is easy.
|
||||
> Prerequesites: [Node.js](https://nodejs.org/) v12, [npm](https://www.npmjs.com/package/npm) and [npx](https://www.npmjs.com/package/npx) must be installed.
|
||||
|
||||
1. run `npm install`.
|
||||
2. run `npx grunt build` (see [`Gruntfile.js`](/Gruntfile.js)). This command:
|
||||
|
||||
1. Cleans the repo.
|
||||
2. Checks code quality ([JSHint](https://jshint.com/) and [ESLint](https://eslint.org/)).
|
||||
3. Runs tests.
|
||||
4. Creates the [distributable](/showdown.js) and [minified](/showdown.min.js) files in the [`dist`](/dist) folder.
|
||||
|
||||
## Tests
|
||||
|
||||
A suite of tests is available which require Node.js. Once Node is installed, run the following command from
|
||||
the project root to install the dependencies:
|
||||
|
||||
npm install
|
||||
|
||||
Once installed the tests can be run from the project root using:
|
||||
|
||||
npm test
|
||||
|
||||
New test cases can easily be added. Create a markdown file (ending in `.md`) which contains the markdown to test.
|
||||
Create a `.html` file of the exact same name. It will automatically be tested when the tests are executed with `mocha`.
|
||||
|
||||
## Contributing
|
||||
|
||||
If you wish to contribute please read the following quick guide.
|
||||
|
||||
### Want a Feature?
|
||||
You can request a new feature by submitting an issue. If you would like to implement a new feature feel free to issue a
|
||||
Pull Request.
|
||||
|
||||
|
||||
### Pull requests (PRs)
|
||||
PRs are awesome. However, before you submit your pull request consider the following guidelines:
|
||||
|
||||
- Search GitHub for an open or closed Pull Request that relates to your submission. You don't want to duplicate effort.
|
||||
- When issuing PRs that change code, make your changes in a new git branch based on master:
|
||||
|
||||
```bash
|
||||
git checkout -b my-fix-branch master
|
||||
```
|
||||
|
||||
- Documentation (i.e: README.md) changes can be made directly against master.
|
||||
- Run the full test suite before submitting and make sure all tests pass (obviously =P).
|
||||
- Try to follow our [**coding style rules**][coding-rules].
|
||||
Breaking them prevents the PR to pass the tests.
|
||||
- Refrain from fixing multiple issues in the same pull request. It's preferable to open multiple small PRs instead of one
|
||||
hard to review big one.
|
||||
- If the PR introduces a new feature or fixes an issue, please add the appropriate test case.
|
||||
- We use commit notes to generate the changelog. It's extremely helpful if your commit messages adhere to the
|
||||
[**AngularJS Git Commit Guidelines**][ng-commit-guide].
|
||||
- If we suggest changes then:
|
||||
- Make the required updates.
|
||||
- Re-run the Angular test suite to ensure tests are still passing.
|
||||
- Rebase your branch and force push to your GitHub repository (this will update your Pull Request):
|
||||
|
||||
```bash
|
||||
git rebase master -i
|
||||
git push origin my-fix-branch -f
|
||||
```
|
||||
- After your pull request is merged, you can safely delete your branch.
|
||||
|
||||
If you have time to contribute to this project, we feel obliged that you get credit for it.
|
||||
These rules enable us to review your PR faster and will give you appropriate credit in your GitHub profile.
|
||||
We thank you in advance for your contribution!
|
||||
|
||||
### Joining the team
|
||||
We're looking for members to help maintaining Showdown.
|
||||
Please see [this issue](https://github.com/showdownjs/showdown/issues/114) to express interest or comment on this note.
|
||||
If you want to contribute to the project, that's awesome! Please check our [guide][contribution-guide].
|
||||
|
||||
## Credits
|
||||
Full credit list at https://github.com/showdownjs/showdown/blob/master/CREDITS.md
|
||||
|
||||
Showdown is powered by:<br/>
|
||||
[![webstorm](https://www.jetbrains.com/webstorm/documentation/docs/logo_webstorm.png)](https://www.jetbrains.com/webstorm/)
|
||||
Full credit list across the versions is available [here][credits].
|
||||
|
||||
## License
|
||||
|
||||
ShowdownJS v 2.0 is release under the MIT version.
|
||||
|
||||
Previous versions are release under BSD.
|
||||
|
||||
----
|
||||
|
||||
[sd-logo]: https://raw.githubusercontent.com/showdownjs/logo/master/dist/logo.readme.png
|
||||
[legacy-branch]: https://github.com/showdownjs/showdown/tree/legacy
|
||||
[releases]: https://github.com/showdownjs/showdown/releases
|
||||
[changelog]: https://github.com/showdownjs/showdown/blob/master/CHANGELOG.md
|
||||
[wiki]: https://github.com/showdownjs/showdown/wiki
|
||||
[cli-wiki]: https://github.com/showdownjs/showdown/wiki/CLI-tool
|
||||
[definitely-typed]: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/showdown
|
||||
[xss-wiki]: https://github.com/showdownjs/showdown/wiki/Markdown's-XSS-Vulnerability-(and-how-to-mitigate-it)
|
||||
[ext-wiki]: https://github.com/showdownjs/showdown/wiki/extensions
|
||||
[coding-rules]: https://github.com/showdownjs/code-style/blob/master/README.md
|
||||
[ng-commit-guide]: https://github.com/showdownjs/code-style/blob/master/README.md#commit-message-convention
|
||||
[boilerplate-repo]: https://github.com/showdownjs/extension-boilerplate
|
||||
[quickstart]: https://showdownjs.com/docs/quickstart/
|
||||
[installation-methods]: https://showdownjs.com/docs/quickstart/#other-installation-methods
|
||||
[extensions]: https://showdownjs.com/docs/extensions/
|
||||
[cli]: https://showdownjs.com/docs/cli/
|
||||
[contribution-guide]: https://showdownjs.com/docs/contribution-guide/
|
||||
[credits]: https://showdownjs.com/docs/credits/
|
|
@ -1,4 +1,8 @@
|
|||
:root {
|
||||
--md-primary-fg-color: rgb(196, 54, 39);
|
||||
--md-accent-fg-color: rgb(62, 139, 138);
|
||||
}
|
||||
|
||||
table {
|
||||
box-sizing: initial;
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
If you wish to contribute, this is your starting point.
|
||||
|
||||
This document contains guidelines to help you make your contribution clear and consistent.
|
||||
These guidelines also help us to review your PR faster and, as a result, will give you
|
||||
appropriate credit in your GitHub profile.
|
||||
|
||||
If you have time to contribute to this project, we are happy to give you credit for it.
|
||||
|
||||
We thank you in advance for your contribution!
|
||||
|
||||
### Features
|
||||
|
||||
You can request a new feature by submitting an issue. If you would like to implement a new feature,
|
||||
feel free to issue a Pull Request.
|
||||
|
||||
### Pull requests
|
||||
|
||||
Pull Requests (PRs) are awesome. Get familiar with the following guidelines before you begin:
|
||||
|
||||
1. Search the project for a Pull Request related to your submission. You don't want to duplicate effort.
|
||||
1. A PR that contains code changes should be created from a git branch based on **develop**:
|
||||
|
||||
```bash
|
||||
git checkout -b my-fix-branch develop
|
||||
```
|
||||
|
||||
1. Follow our [coding style rules][coding-rules].
|
||||
1. Run full test suite and ensure all tests pass.
|
||||
1. If some tests fail, ensure that you follow [coding style rules][coding-rules].
|
||||
1. One PR - one issue. Refrain from fixing multiple issues in the same pull request. Several small PRs are preferable instead of a big one.
|
||||
1. If the PR introduces a new feature or fixes an issue, **please add the appropriate test case(s)**.
|
||||
1. Follow [conventional commit guidelines][conventional-commits] for your commit message(s) when saving changes in your branch and PR.
|
||||
1. Add your name to the [Credits](credits.md) file. We like to give credit where it's due.
|
||||
|
||||
1. If we suggest changes:
|
||||
1. Make the required updates.
|
||||
1. Re-run test suite to ensure tests are still green.
|
||||
1. Rebase your branch and force push to your GitHub repository (this will update your Pull Request):
|
||||
|
||||
```bash
|
||||
git rebase develop -i
|
||||
git push origin my-fix-branch -f
|
||||
```
|
||||
|
||||
1. After your pull request is merged, you can safely delete your branch.
|
||||
|
||||
|
||||
[coding-rules]: https://github.com/showdownjs/code-style/blob/master/README.md
|
||||
[conventional-commits]: https://www.conventionalcommits.org/
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
* [Estevão Santos](https://github.com/tivie)
|
||||
* [SyntaxRules](https://github.com/SyntaxRules)
|
||||
* [Anton Zolotukhin](https://github.com/bandantonio)
|
||||
|
||||
=== "v.1"
|
||||
|
||||
|
|
|
@ -0,0 +1,238 @@
|
|||
## Introduction
|
||||
|
||||
The Event System is the basis of the new Listener extensions, replacing the ["old" extension system](extensions.md) altogether.
|
||||
|
||||
In short, the Event System lifecycle looks as follows:
|
||||
|
||||
1. A sub-parser emits an event.
|
||||
|
||||
!!! note ""
|
||||
Each sub-parser can emit a batch of events (see the list below)
|
||||
|
||||
1. Extension A (which is a _Listener Extension_) registers and listens to a specific event.
|
||||
|
||||
!!! note ""
|
||||
An extension can only register for a specific event
|
||||
|
||||
1. Extension A receives an event object and modifies it.
|
||||
|
||||
!!! note ""
|
||||
Certain properties of the event object can be changed, which will change the behavior or output of the sub-parser
|
||||
|
||||
1. Extension A returns the event object to the converter.
|
||||
1. The converter passes the received event object to the next extension in the chain.
|
||||
|
||||
## Event Object
|
||||
|
||||
### Properties
|
||||
|
||||
#### matches
|
||||
|
||||
**matches** is an object that holds the text captured by the sub-parser. The properties of this object are different for each sub-parser.
|
||||
Some properties can be read-only: their names start with `_` (underscore).
|
||||
|
||||
!!! example "blockquote `onCapture` event"
|
||||
|
||||
```js
|
||||
{
|
||||
_wholeMatch: "> some awesome quote",
|
||||
blockquote: "some awesome quote"
|
||||
}
|
||||
```
|
||||
|
||||
<!-- ## Basic Event -->
|
||||
|
||||
## Event types
|
||||
|
||||
Events are emitted when a sub-parser runs (or about to be run).
|
||||
In the sub-parser, the events follow strict order sequence: [`onStart`](#onstart) -> [`onCapture`](#oncapture) -> [`onHash`](#onhash) -> [`onEnd`](#onend)
|
||||
|
||||
It means that `.before` events always run before `.captureStart`.
|
||||
|
||||
### onStart
|
||||
|
||||
**`<converter>.<subparser>.onStart`**: **always runs** except if the sub-parser is disabled.
|
||||
|
||||
Emitted when the sub-parser has started, but no capturing or modifications to the text were done.
|
||||
|
||||
**Always runs** except if the sub-parser is disabled via options.
|
||||
|
||||
!!! hint "When to use `onStart` event"
|
||||
Use this event when you want to change the input passed to the sub-parser.
|
||||
|
||||
!!! warning ""
|
||||
Please note that the input is the **full text** that was passed to the converter.
|
||||
|
||||
**Properties**
|
||||
|
||||
| property | type | access | description |
|
||||
|--------------|----------|---------|----------------------------------------------------------------|
|
||||
| `input` | `string` | `read` | Full text that was passed to the subparser |
|
||||
| `output` | `string` | `write` | Full text with modification that will be passed along the chain |
|
||||
| `regexp` | `null` | | |
|
||||
| `matches` | `null` | | |
|
||||
| `attributes` | `null` | | |
|
||||
|
||||
### onCapture
|
||||
|
||||
**`<converter>.<subparser>.onCapture`**: *might not be run*.
|
||||
|
||||
Emitted when a regex match is found and capture was successful.
|
||||
Further normalization and modification of the regex captured groups might be performed.
|
||||
|
||||
Might not be run if no regex match found.
|
||||
|
||||
!!! hint "When to use `onCapture` event"
|
||||
Use this event if you want to:
|
||||
|
||||
* modify the sub-parser behavior, text;
|
||||
* modify the HTML output of the sub-parser.
|
||||
|
||||
**Properties**
|
||||
|
||||
| property | type | access | description |
|
||||
|--------------|----------|--------------|-------------------------------------------------------------------|
|
||||
| `input` | `string` | `readonly` | The captured text |
|
||||
| `output` | `string` | `write` | `null` or well-formed HTML (see the Important Note below) |
|
||||
| `regexp` | `RegExp` | `readonly` | Regular Expression to capture groups |
|
||||
| `matches` | `object` | `read/write` | Match groups. Changes to this object are reflected in the output. |
|
||||
| `attributes` | `object` | `read/write` | Attributes to add to the HTML output |
|
||||
|
||||
!!! warning "IMPORTANT NOTE"
|
||||
Extensions listening to the `onCapture` event **should avoid** changing the output property.
|
||||
Instead, they should modify the values of the matches and attribute objects.
|
||||
|
||||
The reason is that the **output property takes precedence over the matches objects** and
|
||||
**prevents showdown from calling other sub-parsers** inside the captured fragment.
|
||||
|
||||
The above means the following:
|
||||
|
||||
1. If something is passed as the output property, any changes to the matches and attributes objects will be ignored.
|
||||
1. Any changes made by other extensions to the matches or attributes objects will be ignored.
|
||||
1. Showdown will not call other sub-parsers, such as encode code or span gamut in the text fragment, which may lead to unexpected results.
|
||||
|
||||
**Example**
|
||||
|
||||
!!! example ""
|
||||
|
||||
```js hl_lines="4"
|
||||
// Showdown extension 1 that is listening to makehtml.blockquote.onCapture
|
||||
function extension_1 (showdownEvent) {
|
||||
// Let's imagine you're a bad person who writes to output
|
||||
showdownEvent.output = '<blockquote>foo</blockquote>'; // must be a well-formed HTML
|
||||
showdownEvent.matches.blockquote = 'some nice quote';
|
||||
}
|
||||
|
||||
// Showdown extension 2 that is also listening to makehtml.blockquote.onCapture
|
||||
function extension_2 (showdownEvent) {
|
||||
// I make blockquotes bold
|
||||
let quote = showdownEvent.matches.blockquote;
|
||||
showdownEvent.matches.blockquote = '<strong>' + quote + '</strong>';
|
||||
}
|
||||
```
|
||||
|
||||
In the example above, the result will always be `<blockquote>foo</blockquote>`, regardless of the order of extension loading and call.
|
||||
|
||||
!!! danger "Infinite loop"
|
||||
Do not pass the input as output to the `onCapture` event, or you might trigger an infinite loop.
|
||||
|
||||
### onHash
|
||||
|
||||
**`<converter>.<subparser>.onHash`**: *always runs*.
|
||||
|
||||
Raised before the output is hashed.
|
||||
|
||||
**Always runs** (except if the sub-parser is disabled via options), even if no hashing is performed.
|
||||
|
||||
!!! hint "When to use `onHash` event"
|
||||
Use this event when you want to change the sub-parser's raw output before it is hashed.
|
||||
|
||||
**Properties**
|
||||
|
||||
| property | type | access | description |
|
||||
|--------------|----------|---------|----------------------------------------------|
|
||||
| `input` | `string` | `read` | The captured text |
|
||||
| `output` | `string` | `write` | The text that will be passed along the chain |
|
||||
| `regexp` | `null` | | |
|
||||
| `matches` | `null` | | |
|
||||
| `attributes` | `null` | | |
|
||||
|
||||
### onEnd
|
||||
|
||||
**`<converter>.<subparser>.onEnd`**: **always runs**;
|
||||
|
||||
Emitted when the sub-parser has finished its work and is about to exit.
|
||||
|
||||
**Always runs** (except if the sub-parser is disabled via options).
|
||||
|
||||
!!! hint "When to use `onEnd` event"
|
||||
Use this event when you want to run code or perform changes to the text after the subparser has run and its output was hashed.
|
||||
|
||||
!!! warning ""
|
||||
Please note that the input is the **full text** and might contain hashed elements.
|
||||
|
||||
**Properties**
|
||||
|
||||
| property | type | access | description |
|
||||
|-----------|----------|---------|-------------------------------------------------------------|
|
||||
| `input` | `string` | `read` | Full text with the subparser modifications (contains hashes) |
|
||||
| `output` | `string` | `write` | The text that will be passed to other subparsers |
|
||||
| `regexp` | `null` | | |
|
||||
| `matches` | `null` | | |
|
||||
|
||||
## Special Events
|
||||
|
||||
There are special events useful for *"positioning"* a listener extension in the main chain of events.
|
||||
Usually, these extensions introduce new syntax that, due to precedence
|
||||
|
||||
In contrary, the special events will always be called, regardless of options or circumstances.
|
||||
|
||||
### .before.{subparserName}
|
||||
|
||||
**`.before.{subparserName}`**: **always runs**
|
||||
|
||||
Emitted just before the **`{subparserName}` is about to be entered**.
|
||||
|
||||
**Properties**
|
||||
|
||||
| property | type | access | description |
|
||||
|-----------|----------|---------|----------------------------------------------------------------|
|
||||
| `input` | `string` | `read` | Full text that was passed to the subparser |
|
||||
| `output` | `string` | `write` | Full text with modification that will be passed along the chain |
|
||||
| `regexp` | `null` | | |
|
||||
| `matches` | `null` | | |
|
||||
|
||||
!!! note "Difference between `before.{subparserName}` and `{subparserName}.start`"
|
||||
|
||||
1. **`before.{subparserName}`** is always guaranteed to be called, **even if the subparser is disabled**,
|
||||
while **{subparserName}.start** isn't.
|
||||
|
||||
For example, `makehtml.before.strikethrough` is always called even if the option `strikethrough` is `false`.
|
||||
|
||||
1. **`before.{subparserName}`** is only emitted **once** in a span context while **`{subparserName}.start`** is emitted
|
||||
everytime **`{subparserName}`** is called.
|
||||
|
||||
<!-- As a rule of thumb -->
|
||||
|
||||
### .after.{subparserName}
|
||||
|
||||
**`.after.{subparserName}`**: **always runs**;
|
||||
|
||||
Emitted when the **`{subparserName}` has exited** and before the next one is called.
|
||||
|
||||
**Properties**
|
||||
|
||||
| property | type | access | description |
|
||||
|-----------|----------|---------|---------------------------------------------------|
|
||||
| `input` | `string` | `read` | Partial/full text with the subparser modifications |
|
||||
| `output` | `string` | `write` | The text that will be passed to other subparsers |
|
||||
| `regexp` | `null` | | |
|
||||
| `matches` | `null` | | |
|
||||
|
||||
<!--
|
||||
## Events List
|
||||
|
||||
### blockquote
|
||||
|
||||
### metadata
|
||||
-->
|
|
@ -1,228 +0,0 @@
|
|||
# Event System )
|
||||
|
||||
## Introduction
|
||||
|
||||
The Event System is the basis of the new Listener extensions, replacing the old extension system altogether.
|
||||
|
||||
The life cycle can be summarized as this:
|
||||
|
||||
- A subparsers emmits an event
|
||||
- Each subparser can emmit a bunch of events (see list below)
|
||||
- Extension A, which is a _Listener Extension_ registers and listens to a specific event
|
||||
- An extension can only register to a specific event
|
||||
- Extension A receives an event object and modifies it
|
||||
- Certain properties of the event object can be changed, ehich will change the behavior or output of the subparser
|
||||
- The extension then returns the event object
|
||||
- The converter passes this event object to the next extension in the chain
|
||||
|
||||
|
||||
Note:
|
||||
|
||||
## The Event Object
|
||||
|
||||
### Properties
|
||||
|
||||
#### matches (type: **object**)
|
||||
|
||||
This object holds the text captured by the subparser. The properties of this object
|
||||
are different for each subparser.
|
||||
|
||||
**Properties whose name starts with `_` are readonly**.
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
// blockquote onCapture event
|
||||
{
|
||||
_wholeMatch: "> some awesome quote",
|
||||
blockquote: "some awesome quote"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Basic Event
|
||||
|
||||
|
||||
## Event Types
|
||||
|
||||
Events are raised when a subparser is run (or about to be run).
|
||||
Within a subparser, the events always follow a certain order (sequence). For instance, **.before** events always run before **.captureStart**.
|
||||
Each subparser raises several events sequentially:
|
||||
|
||||
### onStart
|
||||
|
||||
**`<converter>.<subparser>.onStart`**: **always runs** except if the subparser is disabled
|
||||
|
||||
Raised when the **subparser has started**, but no capturing or any modification to the text was done.
|
||||
**Always runs** (except if the subparser is deactivated through options).
|
||||
|
||||
***Properties***:
|
||||
|
||||
| property | type | access | description |
|
||||
|------------|-----------|------------|--------------------------------------------------------------------|
|
||||
| input | string | read | The full text that was passed to the subparser |
|
||||
| output | string | write | The full text with modification that will be passed along the chain|
|
||||
| regexp | null | | |
|
||||
| matches | null | | |
|
||||
| attributes | null | | |
|
||||
|
||||
|
||||
Usually you would want to use this event if you wish to change the input to the subparser. Please note,
|
||||
however, that the input text is the **complete text** that was fed to the converter.
|
||||
|
||||
### onCapture
|
||||
|
||||
**`<converter>.<subparser>.onCapture`**: *might not be run*;
|
||||
|
||||
Raised when a regex match is found and a capture was successful. Some normalization and modification
|
||||
of the regex captured groups might be performed.
|
||||
|
||||
Might not be run if no regex match is found.
|
||||
|
||||
***Properties***:
|
||||
|
||||
| property | type | access | description |
|
||||
|------------|----------|------------|--------------------------------------------------------------------|
|
||||
| input | string | readonly | The captured text |
|
||||
| output | string | write | null or well formed HTML (see note) |
|
||||
| regexp | RegExp | readonly | Regular Expression used to capture groups |
|
||||
| matches | object | read/write | Matches groups. Changes to this object are reflected in the output |
|
||||
| attributes | object | read/write | Attributes to add to the HTML output |
|
||||
|
||||
Usually you would want to use this event if you wish to modify the subparser behavior, text or modify the HTML output
|
||||
of the subparser.
|
||||
|
||||
**IMPORTANT NOTE**: Extensions listening to onCapture event should try to AVOID changing the output property.
|
||||
Instead, they should modify the values of the matches and attributes objects. This is because
|
||||
the **output property takes precedence over the matches objects** and **prevents showdown to call other subparsers**
|
||||
inside the captured fragment.This means 3 very important things:
|
||||
|
||||
1. If something is passed as the output property, any changes to the matches and attributes objects will be ignored.
|
||||
2. Changes made by other extensions to the matches or attributes objects will also be ignored
|
||||
3. Showdown will not call other subparsers, such as encode code or span gamut in the text fragment, which may lead to
|
||||
weird results.
|
||||
|
||||
```javascript
|
||||
// showdown extension 1 that is listening to makehtml.blockquote.onCapture
|
||||
function extension_1 (showdownEvent) {
|
||||
// i'm bad and write to output
|
||||
showdownEvent.output = '<blockquote>foo</blockquote>'; // must be an well formed html string
|
||||
showdownEvent.matches.blockquote = 'some nice quote';
|
||||
}
|
||||
|
||||
// showdown extension 1 that is also listening to makehtml.blockquote.onCapture
|
||||
function extension_2 (showdownEvent) {
|
||||
// I make blockquotes become bold
|
||||
let quote = showdownEvent.matches.blockquote;
|
||||
showdownEvent.matches.blockquote = '<strong>' + quote + '</strong>';
|
||||
}
|
||||
|
||||
// the result will always be <blockquote>foo</blockquote>, regardless of the order of extension loading and call
|
||||
```
|
||||
|
||||
|
||||
### onHash
|
||||
|
||||
**`<converter>.<subparser>.onHash`**: *always runs*;
|
||||
|
||||
Raised before the output is hashed. **Always runs** (except if the subparser was deactivated through options),
|
||||
even if no hashing is performed.
|
||||
|
||||
***Properties***:
|
||||
|
||||
| property | type | access | description |
|
||||
|------------|------------|------------|--------------------------------------------------------------------|
|
||||
| input | string | read | The captured text |
|
||||
| output | string | write | The text that will be passed to the subparser/other listeners |
|
||||
| regexp | null | | |
|
||||
| matches | null | | |
|
||||
| attributes | null | | |
|
||||
|
||||
Usually you would want to use this event if you wish change the subparser's raw output before it is hashed.
|
||||
|
||||
### onEnd
|
||||
|
||||
**`<converter>.<subparser>.onEnd`**: *always runs*;
|
||||
|
||||
Raised when the subparser has finished its work and is about to exit.
|
||||
|
||||
Always runs (except if the subparser is deactivated through options).
|
||||
|
||||
***Properties***:
|
||||
|
||||
| property | type | access | description |
|
||||
|----------|-----------|------------|--------------------------------------------------------------------|
|
||||
| input | string | read | The full text with the subparser modifications (contains hashes) |
|
||||
| output | string | write | The text that will be passed to other subparsers |
|
||||
| regexp | null | | |
|
||||
| matches | null | | |
|
||||
|
||||
Usually you would want to use this event if you want to run code or perform changes to the text after the subparser was
|
||||
run and it's output was hashed. Keep in mind that the input is the full text and might contain hashed elements.
|
||||
|
||||
|
||||
### Special Events
|
||||
|
||||
There are some special events that are useful for *"positioning"* a listener extension in the main chain of events.
|
||||
Usually these extensions introduce new syntax that, due to precedence
|
||||
These events are always guaranteed to be called, regardless of options or circumstances.
|
||||
|
||||
1. **.before_{subparserName}**: *always runs*
|
||||
|
||||
Raised just before the **{subparserName} is about to be entered**.
|
||||
|
||||
***Properties***:
|
||||
|
||||
| property | type | access | description |
|
||||
|----------|-----------|------------|--------------------------------------------------------------------|
|
||||
| input | string | read | The full text that was passed to the subparser |
|
||||
| output | string | write | The full text with modification that will be passed along the chain|
|
||||
| regexp | null | | |
|
||||
| matches | null | | |
|
||||
|
||||
2. **.after**.{subparserName}: *always runs*;
|
||||
|
||||
Raised when the **{subparserName} has exited** and before the next one is called.
|
||||
|
||||
***Properties***:
|
||||
|
||||
| property | type | access | description |
|
||||
|----------|-----------|------------|--------------------------------------------------------------------|
|
||||
| input | string | read | The partial/full text with the subparser modifications |
|
||||
| output | string | write | The text that will be passed to other subparsers |
|
||||
| regexp | null | | |
|
||||
| matches | null | | |
|
||||
|
||||
|
||||
### Notes
|
||||
|
||||
- There are 2 main differences between **before.{subparserName}** and **{subparserName}.start**.
|
||||
|
||||
1. **before.{subparserName}** is always guaranteed to be called, even if the subparser is disabled,
|
||||
while **{subparserName}.start** doesn't.
|
||||
|
||||
ex: `makehtml.before.strikethrough` is always called even if the option `strikethrough` is false
|
||||
|
||||
2. **before.{subparserName}** is only raised once in a span context while **{subparserName}.start** is raised
|
||||
everytime **{subparserName}** is called.
|
||||
|
||||
As a rule of thumb,
|
||||
|
||||
## Events List
|
||||
|
||||
|
||||
### blockquote
|
||||
|
||||
####
|
||||
|
||||
|
||||
### metadata
|
||||
|
||||
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
### Infinite loop
|
||||
|
||||
If you pass the input as output in a `onCapture` event, you might trigger an infinite loop.
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
Showdown allows you to load additional functionality via extensions. You can find a list of known Showdown extensions [here][ext-wiki].
|
||||
Showdown allows you to load additional functionality via extensions. You can find a list of known Showdown extensions [here][ext-list].
|
||||
|
||||
You can also check the [boilerplate repo][boilerplate-repo], to create your own extension(s).
|
||||
|
||||
|
@ -32,5 +32,5 @@ You can also check the [boilerplate repo][boilerplate-repo], to create your own
|
|||
showdown -e twitter -i foo.md -o bar.html
|
||||
```
|
||||
|
||||
[ext-wiki]: https://github.com/showdownjs/showdown/wiki/extensions
|
||||
[ext-list]: extensions-list.md
|
||||
[boilerplate-repo]: https://github.com/showdownjs/extension-boilerplate
|
|
@ -31,8 +31,7 @@ Showdown can be used on the client-side (in the browser) or server-side (with No
|
|||
|
||||
## Installation
|
||||
|
||||
To install Showdown, follow the instructions from the [Installation guide](installation.md).
|
||||
|
||||
To install Showdown, follow the instructions from the [quickstart guide](quickstart.md).
|
||||
|
||||
## License
|
||||
|
||||
|
@ -40,12 +39,4 @@ ShowdownJS v 2.0 is release under the MIT version.
|
|||
|
||||
Previous versions are release under BSD.
|
||||
|
||||
[sd-logo]: https://raw.githubusercontent.com/showdownjs/logo/master/dist/logo.readme.png
|
||||
[wiki]: https://github.com/showdownjs/showdown/wiki
|
||||
[cli-wiki]: https://github.com/showdownjs/showdown/wiki/CLI-tool
|
||||
[definitely-typed]: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/showdown
|
||||
[xss-wiki]: https://github.com/showdownjs/showdown/wiki/Markdown's-XSS-Vulnerability-(and-how-to-mitigate-it)
|
||||
[ext-wiki]: https://github.com/showdownjs/showdown/wiki/extensions
|
||||
[coding-rules]: https://github.com/showdownjs/code-style/blob/master/README.md
|
||||
[ng-commit-guide]: https://github.com/showdownjs/code-style/blob/master/README.md#commit-message-convention
|
||||
[boilerplate-repo]: https://github.com/showdownjs/extension-boilerplate
|
||||
[sd-logo]: https://raw.githubusercontent.com/showdownjs/logo/master/dist/logo.readme.png
|
|
@ -0,0 +1,42 @@
|
|||
## Prerequesites
|
||||
|
||||
* [Node.js](https://nodejs.org/) v12+
|
||||
* [npm](https://www.npmjs.com/package/npm)
|
||||
* [npx](https://www.npmjs.com/package/npx)
|
||||
|
||||
## Build
|
||||
|
||||
Building your clone of the repository is easy:
|
||||
|
||||
1. run `npm install`.
|
||||
1. run `npx grunt build` (see [`Gruntfile.js`][gruntfile]). This command:
|
||||
|
||||
1. Cleans the repo.
|
||||
1. Checks code quality ([JSHint](https://jshint.com/) and [ESLint](https://eslint.org/)).
|
||||
1. Runs tests.
|
||||
1. Creates the [distributable][sd-dist] and [minified][sd-min] files in the [`dist`][dist-folder] folder.
|
||||
|
||||
## Test
|
||||
|
||||
A suite of tests is available which require Node.js. Once Node is installed, run the following command from
|
||||
the project root to install the dependencies:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
Once installed, run tests from the project root:
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
|
||||
You can easily add new tests:
|
||||
|
||||
1. Create a markdown file (ending in `.md`) that contains the markdown to test.
|
||||
1. Create a `.html` file with the same name as the markdown one from the previous step. This `html` file will automatically be tested when the tests are executed with `mocha`.
|
||||
|
||||
[gruntfile]: https://github.com/showdownjs/showdown/blob/master/Gruntfile.js
|
||||
[sd-dist]: https://github.com/showdownjs/showdown/blob/master/dist/showdown.js
|
||||
[sd-min]: https://github.com/showdownjs/showdown/blob/master/dist/showdown.min.js
|
||||
[dist-folder]: https://github.com/showdownjs/showdown/tree/master/dist
|
|
@ -30,6 +30,7 @@ site_dir: public
|
|||
nav:
|
||||
- Home:
|
||||
- Introduction: index.md
|
||||
- Contribution guide: contribution-guide.md
|
||||
- Donations: donations.md
|
||||
- Credits: credits.md
|
||||
- Quickstart:
|
||||
|
@ -42,8 +43,10 @@ nav:
|
|||
- Flavors: flavors.md
|
||||
- CLI: cli.md
|
||||
- Integrations: integrations.md
|
||||
- Event system: event-system.md
|
||||
- Extensions:
|
||||
- Overview: extensions.md
|
||||
- Create an extension: create-extension.md
|
||||
- List of known extensions: extensions-list.md
|
||||
- Tutorials: tutorials/index.md
|
||||
- Tutorials: tutorials/index.md
|
||||
- Local development: local-development.md
|
|
@ -23,10 +23,11 @@ showdown.subParser('makehtml.blockGamut', function (text, options, globals, skip
|
|||
startEvent = globals.converter.dispatch(startEvent);
|
||||
text = startEvent.output;
|
||||
|
||||
// we parse blockquotes first so that we can have headings and hrs
|
||||
// inside blockquotes
|
||||
if (skip !== 'makehtml.heading') {
|
||||
text = showdown.subParser('makehtml.heading')(text, options, globals);
|
||||
if (skip !== 'makehtml.heading.setext') {
|
||||
text = showdown.subParser('makehtml.heading.setext')(text, options, globals);
|
||||
}
|
||||
if (skip !== 'makehtml.heading.atx') {
|
||||
text = showdown.subParser('makehtml.heading.atx')(text, options, globals);
|
||||
}
|
||||
|
||||
// Do Horizontal Rules:
|
||||
|
|
|
@ -90,8 +90,7 @@ showdown.subParser('makehtml.codeSpan', function (text, options, globals) {
|
|||
beforeHashEvent = globals.converter.dispatch(beforeHashEvent);
|
||||
otp = beforeHashEvent.output;
|
||||
return showdown.subParser('makehtml.hashHTMLSpans')(otp, options, globals);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
let afterEvent = new showdown.Event('makehtml.codeSpan.onEnd', text);
|
||||
afterEvent
|
||||
|
|
|
@ -33,6 +33,9 @@ showdown.subParser('makehtml.encodeAmpsAndAngles', function (text, options, glob
|
|||
// Encode >
|
||||
text = text.replace(/>/g, '>');
|
||||
|
||||
// encode "
|
||||
text = text.replace(/"/g, '"');
|
||||
|
||||
let afterEvent = new showdown.Event('makehtml.encodeAmpsAndAngles.onEnd', text);
|
||||
afterEvent
|
||||
.setOutput(text)
|
||||
|
|
|
@ -29,6 +29,8 @@ showdown.subParser('makehtml.encodeCode', function (text, options, globals) {
|
|||
// Do the angle bracket song and dance:
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
// encode "
|
||||
.replace(/"/g, '"')
|
||||
// Now, escape characters that are magic in Markdown:
|
||||
.replace(/([*_{}\[\]\\=~-])/g, showdown.helper.escapeCharactersCallback);
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ showdown.subParser('makehtml.escapeSpecialCharsWithinTagAttributes', function (t
|
|||
text = startEvent.output;
|
||||
|
||||
// Build a regex to find HTML tags.
|
||||
let tags = /<\/?[a-z\d_:-]+(?:[\s]+[\s\S]+?)?>/gi,
|
||||
let tags = /<\/?[a-z\d_:-]+(?:\s+[\s\S]+?)?>/gi,
|
||||
comments = /<!(--(([^>-]|-[^>])([^-]|-[^-])*)--)>/gi;
|
||||
|
||||
text = text.replace(tags, function (wholeMatch) {
|
||||
|
|
|
@ -21,175 +21,20 @@
|
|||
// ***Author:***
|
||||
// - Estêvão Soares dos Santos (Tivie) <https://github.com/tivie>
|
||||
////
|
||||
(function () {
|
||||
|
||||
showdown.subParser('makehtml.heading', function (text, options, globals) {
|
||||
'use strict';
|
||||
|
||||
let startEvent = new showdown.Event('makehtml.heading.onStart', text);
|
||||
startEvent
|
||||
.setOutput(text)
|
||||
._setGlobals(globals)
|
||||
._setOptions(options);
|
||||
startEvent = globals.converter.dispatch(startEvent);
|
||||
text = startEvent.output;
|
||||
|
||||
let setextRegexH1 = /^( {0,3}([^ \t\n]+.*\n)(.+\n)?(.+\n)?)( {0,3}=+[ \t]*)$/gm,
|
||||
setextRegexH2 = /^( {0,3}([^ \t\n]+.*\n)(.+\n)?(.+\n)?)( {0,3}(-+)[ \t]*)$/gm,
|
||||
atxRegex = (options.requireSpaceBeforeHeadingText) ? /^ {0,3}(#{1,6})[ \t]+(.+?)(?:[ \t]+#+)?[ \t]*$/gm : /^ {0,3}(#{1,6})[ \t]*(.+?)[ \t]*#*[ \t]*$/gm;
|
||||
|
||||
text = text.replace(setextRegexH1, function (wholeMatch, headingText, line1, line2, line3, line4) {
|
||||
return parseSetextHeading(setextRegexH2, options.headerLevelStart, wholeMatch, headingText, line1, line2, line3, line4);
|
||||
});
|
||||
|
||||
text = text.replace(setextRegexH2, function (wholeMatch, headingText, line1, line2, line3, line4) {
|
||||
return parseSetextHeading(setextRegexH2, options.headerLevelStart + 1, wholeMatch, headingText, line1, line2, line3, line4);
|
||||
});
|
||||
|
||||
text = text.replace(atxRegex, function (wholeMatch, m1, m2) {
|
||||
let headingLevel = options.headerLevelStart - 1 + m1.length,
|
||||
headingText = (options.customizedHeaderId) ? m2.replace(/\s?{([^{]+?)}\s*$/, '') : m2,
|
||||
id = (options.noHeaderId) ? null : showdown.subParser('makehtml.heading.id')(m2, options, globals);
|
||||
return parseHeader(setextRegexH2, wholeMatch, headingText, headingLevel, id);
|
||||
});
|
||||
|
||||
let afterEvent = new showdown.Event('makehtml.heading.onEnd', text);
|
||||
afterEvent
|
||||
.setOutput(text)
|
||||
._setGlobals(globals)
|
||||
._setOptions(options);
|
||||
afterEvent = globals.converter.dispatch(afterEvent);
|
||||
return afterEvent.output;
|
||||
|
||||
|
||||
|
||||
function parseSetextHeading (pattern, headingLevel, wholeMatch, headingText, line1, line2, line3, line4) {
|
||||
|
||||
// count lines
|
||||
let count = headingText.trim().split('\n').length;
|
||||
let prepend = '';
|
||||
let nPrepend;
|
||||
const hrCheckRgx = /^ {0,3}[-_*]([-_*] ?){2,}$/;
|
||||
|
||||
// one liner edge cases
|
||||
if (count === 1) {
|
||||
// hr
|
||||
// let's find the hr edge case first
|
||||
if (showdown.helper.trimEnd(line1).match(hrCheckRgx)) {
|
||||
// it's the edge case, so it's a false positive
|
||||
prepend = showdown.subParser('makehtml.horizontalRule')(line1, options, globals);
|
||||
if (prepend !== line1) {
|
||||
// it's an oneliner list
|
||||
return prepend.trim() + '\n' + line4;
|
||||
}
|
||||
}
|
||||
|
||||
// now check if it's an unordered list
|
||||
if (line1.match(/^ {0,3}[-*+][ \t]/)) {
|
||||
if (line4.trim().match(/^=+/)) {
|
||||
line1 += line4;
|
||||
}
|
||||
prepend = showdown.subParser('makehtml.list')(line1, options, globals);
|
||||
if (prepend !== line1) {
|
||||
// it's an oneliner list
|
||||
return prepend.trim() + '\n' + line4;
|
||||
}
|
||||
}
|
||||
|
||||
// check if it's a blockquote
|
||||
if (line1.match(/^ {0,3}>[ \t]?[^ \t]/)) {
|
||||
if (line4.trim().match(/^=+/)) {
|
||||
line1 += line4;
|
||||
}
|
||||
prepend = showdown.subParser('makehtml.blockquote')(line1, options, globals);
|
||||
if (prepend !== line1) {
|
||||
// it's an oneliner blockquote
|
||||
|
||||
return prepend.trim() + '\n' + line4;
|
||||
}
|
||||
}
|
||||
|
||||
// no edge case let's proceed as usual
|
||||
} else {
|
||||
let multilineText = '';
|
||||
|
||||
// multiline is a bit trickier
|
||||
// first we must take care of the edge cases of:
|
||||
// case1: | case2:
|
||||
// --- | ---
|
||||
// foo | foo
|
||||
// --- | bar
|
||||
// | ---
|
||||
//
|
||||
if (showdown.helper.trimEnd(line1).match(hrCheckRgx)) {
|
||||
nPrepend = showdown.subParser('makehtml.horizontalRule')(line1, options, globals);
|
||||
if (nPrepend !== line1) {
|
||||
line1 = '';
|
||||
// we add the parsed block to prepend
|
||||
prepend = nPrepend.trim();
|
||||
// and remove the line from the headingText, so it doesn't appear repeated
|
||||
headingText = line2 + ((line3) ? line3 : '');
|
||||
}
|
||||
}
|
||||
|
||||
// now we take care of these cases:
|
||||
// case1: | case2:
|
||||
// foo | foo
|
||||
// *** | ***
|
||||
// --- | bar
|
||||
// | ---
|
||||
//
|
||||
if (showdown.helper.trimEnd(line2).match(hrCheckRgx)) {
|
||||
// This case sucks, because the first line could be anything!!!
|
||||
// first let's make sure it's a hr
|
||||
nPrepend = showdown.subParser('makehtml.horizontalRule')(line2, options, globals);
|
||||
if (nPrepend !== line2) {
|
||||
line2 = nPrepend;
|
||||
// it is, so now we must parse line1 also
|
||||
if (line1) {
|
||||
line1 = showdown.subParser('makehtml.blockGamut')(line1, options, globals);
|
||||
line1 = showdown.subParser('makehtml.paragraphs')(line1, options, globals);
|
||||
line1 = line1.trim() + '\n';
|
||||
prepend = line1;
|
||||
// and clear line1
|
||||
line1 = '';
|
||||
}
|
||||
// we add the parsed blocks to prepend
|
||||
prepend += line2.trim() + '\n';
|
||||
line2 = '';
|
||||
// and remove the lines from the headingText, so it doesn't appear repeated
|
||||
headingText = (line3) ? line3 : '';
|
||||
}
|
||||
}
|
||||
|
||||
// all edge cases should be treated now
|
||||
multilineText = line1 + line2 + ((line3) ? line3 : '');
|
||||
if (line4.trim().match(/^=+/)) {
|
||||
multilineText += line4;
|
||||
}
|
||||
|
||||
nPrepend = showdown.subParser('makehtml.blockGamut')(multilineText, options, globals, 'makehtml.heading');
|
||||
if (nPrepend !== multilineText) {
|
||||
// we found a block, so it should take precedence
|
||||
prepend += nPrepend;
|
||||
headingText = '';
|
||||
}
|
||||
}
|
||||
|
||||
// trim stuff
|
||||
headingText = headingText.trim();
|
||||
|
||||
// let's check if heading is empty
|
||||
// after looking for blocks, heading text might be empty which is a false positive
|
||||
if (!headingText) {
|
||||
return prepend + line4;
|
||||
}
|
||||
|
||||
// after this, we're pretty sure it's a heading so let's proceed
|
||||
let id = (options.noHeaderId) ? null : showdown.subParser('makehtml.heading.id')(headingText, options, globals);
|
||||
return prepend + parseHeader(pattern, wholeMatch, headingText, headingLevel, id);
|
||||
}
|
||||
|
||||
function parseHeader (pattern, wholeMatch, headingText, headingLevel, headingId) {
|
||||
/**
|
||||
*
|
||||
* @param {RegExp} pattern
|
||||
* @param {string} wholeMatch
|
||||
* @param {string} headingText
|
||||
* @param {string} headingLevel
|
||||
* @param {string} headingId
|
||||
* @param {{}} options
|
||||
* @param {{}} globals
|
||||
* @returns {string}
|
||||
*/
|
||||
function parseHeader (pattern, wholeMatch, headingText, headingLevel, headingId, options, globals) {
|
||||
let captureStartEvent = new showdown.Event('makehtml.heading.onCapture', headingText),
|
||||
otp;
|
||||
|
||||
|
@ -229,70 +74,300 @@ showdown.subParser('makehtml.heading', function (text, options, globals) {
|
|||
return showdown.subParser('makehtml.hashBlock')(otp, options, globals);
|
||||
}
|
||||
|
||||
});
|
||||
showdown.subParser('makehtml.heading', function (text, options, globals) {
|
||||
'use strict';
|
||||
|
||||
showdown.subParser('makehtml.heading.id', function (m, options, globals) {
|
||||
let title,
|
||||
prefix;
|
||||
let startEvent = new showdown.Event('makehtml.heading.onStart', text);
|
||||
startEvent
|
||||
.setOutput(text)
|
||||
._setGlobals(globals)
|
||||
._setOptions(options);
|
||||
startEvent = globals.converter.dispatch(startEvent);
|
||||
text = startEvent.output;
|
||||
|
||||
// It is separate from other options to allow combining prefix and customized
|
||||
if (options.customizedHeaderId) {
|
||||
let match = m.match(/{([^{]+?)}\s*$/);
|
||||
if (match && match[1]) {
|
||||
m = match[1];
|
||||
text = showdown.subParser('makehtml.heading.setext')(text, options, globals);
|
||||
text = showdown.subParser('makehtml.heading.atx')(text, options, globals);
|
||||
|
||||
let afterEvent = new showdown.Event('makehtml.heading.onEnd', text);
|
||||
afterEvent
|
||||
.setOutput(text)
|
||||
._setGlobals(globals)
|
||||
._setOptions(options);
|
||||
afterEvent = globals.converter.dispatch(afterEvent);
|
||||
return afterEvent.output;
|
||||
|
||||
});
|
||||
|
||||
showdown.subParser('makehtml.heading.id', function (m, options, globals) {
|
||||
let title,
|
||||
prefix;
|
||||
|
||||
// It is separate from other options to allow combining prefix and customized
|
||||
if (options.customizedHeaderId) {
|
||||
let match = m.match(/{([^{]+?)}\s*$/);
|
||||
if (match && match[1]) {
|
||||
m = match[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
title = m;
|
||||
title = m;
|
||||
|
||||
// Prefix id to prevent causing inadvertent pre-existing style matches.
|
||||
if (showdown.helper.isString(options.prefixHeaderId)) {
|
||||
prefix = options.prefixHeaderId;
|
||||
} else if (options.prefixHeaderId === true) {
|
||||
prefix = 'section-';
|
||||
} else {
|
||||
prefix = '';
|
||||
}
|
||||
// Prefix id to prevent causing inadvertent pre-existing style matches.
|
||||
if (showdown.helper.isString(options.prefixHeaderId)) {
|
||||
prefix = options.prefixHeaderId;
|
||||
} else if (options.prefixHeaderId === true) {
|
||||
prefix = 'section-';
|
||||
} else {
|
||||
prefix = '';
|
||||
}
|
||||
|
||||
if (!options.rawPrefixHeaderId) {
|
||||
title = prefix + title;
|
||||
}
|
||||
if (!options.rawPrefixHeaderId) {
|
||||
title = prefix + title;
|
||||
}
|
||||
|
||||
if (options.ghCompatibleHeaderId) {
|
||||
title = title
|
||||
.replace(/ /g, '-')
|
||||
// replace previously escaped chars (&, ¨ and $)
|
||||
.replace(/&/g, '')
|
||||
.replace(/¨T/g, '')
|
||||
.replace(/¨D/g, '')
|
||||
// replace rest of the chars (&~$ are repeated as they might have been escaped)
|
||||
// borrowed from github's redcarpet (so they should produce similar results)
|
||||
.replace(/[&+$,\/:;=?@"#{}|^¨~\[\]`\\*)(%.!'<>]/g, '')
|
||||
.toLowerCase();
|
||||
} else if (options.rawHeaderId) {
|
||||
title = title
|
||||
.replace(/ /g, '-')
|
||||
// replace previously escaped chars (&, ¨ and $)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/¨T/g, '¨')
|
||||
.replace(/¨D/g, '$')
|
||||
// replace " and '
|
||||
.replace(/["']/g, '-')
|
||||
.toLowerCase();
|
||||
} else {
|
||||
title = title
|
||||
.replace(/\W/g, '')
|
||||
.toLowerCase();
|
||||
}
|
||||
if (options.ghCompatibleHeaderId) {
|
||||
title = title
|
||||
.replace(/ /g, '-')
|
||||
// replace previously escaped chars (&, ¨ and $)
|
||||
.replace(/&/g, '')
|
||||
.replace(/¨T/g, '')
|
||||
.replace(/¨D/g, '')
|
||||
// replace rest of the chars (&~$ are repeated as they might have been escaped)
|
||||
// borrowed from github's redcarpet (so they should produce similar results)
|
||||
.replace(/[&+$,\/:;=?@"#{}|^¨¿?:~\[\]`、゠=…‥『』〝〟「」\\*(){}()[]【】%.。,¡!!'<>]/g, '')
|
||||
.toLowerCase();
|
||||
} else if (options.rawHeaderId) {
|
||||
title = title
|
||||
.replace(/ /g, '-')
|
||||
// replace previously escaped chars (&, ¨ and $)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/¨T/g, '¨')
|
||||
.replace(/¨D/g, '$')
|
||||
// replace " and '
|
||||
.replace(/["']/g, '-')
|
||||
.toLowerCase();
|
||||
} else {
|
||||
title = title
|
||||
.replace(/\W/g, '')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
if (options.rawPrefixHeaderId) {
|
||||
title = prefix + title;
|
||||
}
|
||||
if (options.rawPrefixHeaderId) {
|
||||
title = prefix + title;
|
||||
}
|
||||
|
||||
if (globals.hashLinkCounts[title]) {
|
||||
title = title + '-' + (globals.hashLinkCounts[title]++);
|
||||
} else {
|
||||
globals.hashLinkCounts[title] = 1;
|
||||
}
|
||||
return title;
|
||||
});
|
||||
if (globals.hashLinkCounts[title]) {
|
||||
title = title + '-' + (globals.hashLinkCounts[title]++);
|
||||
} else {
|
||||
globals.hashLinkCounts[title] = 1;
|
||||
}
|
||||
return title;
|
||||
});
|
||||
|
||||
showdown.subParser('makehtml.heading.setext', function (text, options, globals) {
|
||||
|
||||
let startEvent = new showdown.Event('makehtml.heading.setext.onStart', text);
|
||||
startEvent
|
||||
.setOutput(text)
|
||||
._setGlobals(globals)
|
||||
._setOptions(options);
|
||||
startEvent = globals.converter.dispatch(startEvent);
|
||||
text = startEvent.output;
|
||||
|
||||
const setextRegexH1 = /^( {0,3}([^ \t\n]+.*\n)(.+\n)?(.+\n)?)( {0,3}=+[ \t]*)$/gm,
|
||||
setextRegexH2 = /^( {0,3}([^ \t\n]+.*\n)(.+\n)?(.+\n)?)( {0,3}(-+)[ \t]*)$/gm;
|
||||
|
||||
text = text.replace(setextRegexH1, function (wholeMatch, headingText, line1, line2, line3, line4) {
|
||||
return parseSetextHeading(setextRegexH2, options.headerLevelStart, wholeMatch, headingText, line1, line2, line3, line4);
|
||||
});
|
||||
|
||||
text = text.replace(setextRegexH2, function (wholeMatch, headingText, line1, line2, line3, line4) {
|
||||
return parseSetextHeading(setextRegexH2, options.headerLevelStart + 1, wholeMatch, headingText, line1, line2, line3, line4);
|
||||
});
|
||||
|
||||
let afterEvent = new showdown.Event('makehtml.heading.setext.onEnd', text);
|
||||
afterEvent
|
||||
.setOutput(text)
|
||||
._setGlobals(globals)
|
||||
._setOptions(options);
|
||||
afterEvent = globals.converter.dispatch(afterEvent);
|
||||
|
||||
return showdown.subParser('makehtml.hashHTMLBlocks')(afterEvent.output, options, globals);
|
||||
|
||||
|
||||
function parseSetextHeading (pattern, headingLevel, wholeMatch, headingText, line1, line2, line3, line4) {
|
||||
|
||||
// count lines
|
||||
let count = headingText.trim().split('\n').length;
|
||||
let prepend = '';
|
||||
let nPrepend;
|
||||
const hrCheckRgx = /^ {0,3}[-_*]([-_*] ?){2,}$/;
|
||||
|
||||
// one liner edge cases
|
||||
if (count === 1) {
|
||||
// hr
|
||||
// let's find the hr edge case first
|
||||
if (showdown.helper.trimEnd(line1).match(hrCheckRgx)) {
|
||||
// it's the edge case, so it's a false positive
|
||||
prepend = showdown.subParser('makehtml.horizontalRule')(line1, options, globals);
|
||||
if (prepend !== line1) {
|
||||
// it's an oneliner list
|
||||
return prepend.trim() + '\n' + line4;
|
||||
}
|
||||
}
|
||||
|
||||
// now check if it's an unordered list
|
||||
if (line1.match(/^ {0,3}[-*+][ \t]/)) {
|
||||
if (line4.trim().match(/^=+/)) {
|
||||
line1 += line4;
|
||||
}
|
||||
prepend = showdown.subParser('makehtml.list')(line1, options, globals);
|
||||
if (prepend !== line1) {
|
||||
// it's an oneliner list
|
||||
return prepend.trim() + '\n' + line4;
|
||||
}
|
||||
}
|
||||
|
||||
// check if it's a blockquote
|
||||
if (line1.match(/^ {0,3}>[ \t]?[^ \t]/)) {
|
||||
if (line4.trim().match(/^=+/)) {
|
||||
line1 += line4;
|
||||
}
|
||||
prepend = showdown.subParser('makehtml.blockquote')(line1, options, globals);
|
||||
if (prepend !== line1) {
|
||||
// it's an oneliner blockquote
|
||||
return prepend.trim() + '\n' + line4;
|
||||
}
|
||||
}
|
||||
|
||||
// no edge case let's proceed as usual
|
||||
} else {
|
||||
let multilineText = '';
|
||||
|
||||
// multiline is a bit trickier
|
||||
// first we must take care of the edge cases of:
|
||||
// case1: | case2:
|
||||
// --- | ---
|
||||
// foo | foo
|
||||
// --- | bar
|
||||
// | ---
|
||||
//
|
||||
if (showdown.helper.trimEnd(line1).match(hrCheckRgx)) {
|
||||
nPrepend = showdown.subParser('makehtml.horizontalRule')(line1, options, globals);
|
||||
if (nPrepend !== line1) {
|
||||
line1 = '';
|
||||
// we add the parsed block to prepend
|
||||
prepend = nPrepend.trim();
|
||||
// and remove the line from the headingText, so it doesn't appear repeated
|
||||
headingText = line2 + ((line3) ? line3 : '');
|
||||
}
|
||||
}
|
||||
|
||||
// now we take care of these cases:
|
||||
// case1: | case2:
|
||||
// foo | foo
|
||||
// *** | ***
|
||||
// --- | bar
|
||||
// | ---
|
||||
//
|
||||
if (showdown.helper.trimEnd(line2).match(hrCheckRgx)) {
|
||||
// This case sucks, because the first line could be anything!!!
|
||||
// first let's make sure it's a hr
|
||||
nPrepend = showdown.subParser('makehtml.horizontalRule')(line2, options, globals);
|
||||
if (nPrepend !== line2) {
|
||||
line2 = nPrepend;
|
||||
// it is, so now we must parse line1 also
|
||||
if (line1) {
|
||||
line1 = showdown.subParser('makehtml.blockGamut')(line1, options, globals);
|
||||
line1 = showdown.subParser('makehtml.paragraphs')(line1, options, globals);
|
||||
line1 = line1.trim() + '\n';
|
||||
prepend = line1;
|
||||
// and clear line1
|
||||
line1 = '';
|
||||
}
|
||||
// we add the parsed blocks to prepend
|
||||
prepend += line2.trim() + '\n';
|
||||
line2 = '';
|
||||
// and remove the lines from the headingText, so it doesn't appear repeated
|
||||
headingText = (line3) ? line3 : '';
|
||||
}
|
||||
}
|
||||
|
||||
// all edge cases should be treated now
|
||||
multilineText = line1 + line2 + ((line3) ? line3 : '');
|
||||
//if (line4.trim().match(/^=+/)) {
|
||||
// multilineText += line4;
|
||||
//}
|
||||
|
||||
nPrepend = showdown.subParser('makehtml.blockGamut')(multilineText, options, globals, 'makehtml.heading.setext');
|
||||
if (nPrepend !== multilineText) {
|
||||
// we found one or more blocks, so we need to reparse (blocks should take precendence though)
|
||||
nPrepend = showdown.helper.trimEnd(nPrepend);
|
||||
// let's check if the last line is a parsed block
|
||||
let newLines = nPrepend.trim().split('\n');
|
||||
let nLastLine = newLines.pop().toString();
|
||||
|
||||
if (/^¨K\d+K$/.test(nLastLine) || /^\s*$/gm.test(nLastLine)) {
|
||||
// everything before --- or === is a block or empty line, so it's a false positive
|
||||
prepend += nPrepend + '\n\n';
|
||||
headingText = '';
|
||||
} else {
|
||||
// the last line is something else... so let's look at the line before that
|
||||
let toHeading = nLastLine;
|
||||
nLastLine = newLines.pop().toString();
|
||||
if (/^¨K\d+K$/.test(nLastLine) === false && /^\s*$/gm.test(nLastLine) === false) {
|
||||
toHeading = nLastLine + '\n' + toHeading;
|
||||
}
|
||||
headingText = toHeading;
|
||||
prepend = newLines.join('\n').trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// trim stuff
|
||||
headingText = headingText.trim();
|
||||
|
||||
// let's check if heading is empty
|
||||
// after looking for blocks, heading text might be empty which is a false positive
|
||||
if (!headingText) {
|
||||
return prepend + line4;
|
||||
}
|
||||
|
||||
// after this, we're pretty sure it's a heading so let's proceed
|
||||
let id = (options.noHeaderId) ? null : showdown.subParser('makehtml.heading.id')(headingText, options, globals);
|
||||
return prepend + parseHeader(pattern, wholeMatch, headingText, headingLevel, id, options, globals);
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
showdown.subParser('makehtml.heading.atx', function (text, options, globals) {
|
||||
|
||||
let startEvent = new showdown.Event('makehtml.heading.atx.onStart', text);
|
||||
startEvent
|
||||
.setOutput(text)
|
||||
._setGlobals(globals)
|
||||
._setOptions(options);
|
||||
startEvent = globals.converter.dispatch(startEvent);
|
||||
text = startEvent.output;
|
||||
|
||||
const atxRegex = (options.requireSpaceBeforeHeadingText) ? /^ {0,3}(#{1,6})[ \t]+(.+?)(?:[ \t]+#+)?[ \t]*$/gm : /^ {0,3}(#{1,6})[ \t]*(.+?)[ \t]*#*[ \t]*$/gm;
|
||||
text = text.replace(atxRegex, function (wholeMatch, m1, m2) {
|
||||
let headingLevel = options.headerLevelStart - 1 + m1.length,
|
||||
headingText = (options.customizedHeaderId) ? m2.replace(/\s?{([^{]+?)}\s*$/, '') : m2,
|
||||
id = (options.noHeaderId) ? null : showdown.subParser('makehtml.heading.id')(m2, options, globals);
|
||||
return parseHeader(atxRegex, wholeMatch, headingText, headingLevel, id, options, globals);
|
||||
});
|
||||
|
||||
let afterEvent = new showdown.Event('makehtml.heading.atx.onEnd', text);
|
||||
afterEvent
|
||||
.setOutput(text)
|
||||
._setGlobals(globals)
|
||||
._setOptions(options);
|
||||
afterEvent = globals.converter.dispatch(afterEvent);
|
||||
|
||||
return showdown.subParser('makehtml.hashHTMLBlocks')(afterEvent.output, options, globals);
|
||||
|
||||
});
|
||||
|
||||
})();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<h1 id="some-header">some header</h1>
|
||||
<h1 id="some-header-with--chars">some header with &+$,/:;=?@"#{}|^~[]`\*()%.!' chars</h1>
|
||||
<h1 id="some-header-with--chars">some header with &+$,/:;=?@"#{}|^¨¿?:~[]`゠=…‥『』〝〟「」\*(){}()[]【】%.。,¡!!' chars</h1>
|
||||
<h1 id="another-header--with--chars">another header > with < chars</h1>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# some header
|
||||
|
||||
# some header with &+$,/:;=?@"#{}|^~[]`\\*()%.!' chars
|
||||
# some header with &+$,/:;=?@\"#{}|^¨¿?:~[]`゠=…‥『』〝〟「」\\*(){}()[]【】%.。,¡!!' chars
|
||||
|
||||
# another header > with < chars
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
<p>this is some text</p>
|
||||
<p><code>php
|
||||
function thisThing() {
|
||||
echo "some weird formatted code!";
|
||||
}
|
||||
</code></p>
|
||||
<p><code>php function thisThing() { echo "some weird formatted code!"; } </code></p>
|
||||
<p>some other text</p>
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
<h1 id="/prefix/some-header">some header</h1>
|
||||
<h1 id="/prefix/another-!-#$%&/()=?»@£§{[]}«--header">another !"#$%&/()=?»@£§{[]}«' header</h1>
|
||||
<h1 id="/prefix/another-!-#$%&/()=?»@£§{[]}«--header">another !"#$%&/()=?»@£§{[]}«' header</h1>
|
||||
|
|
|
@ -75,7 +75,7 @@ ___triple underscores___
|
|||
<p>Which gives you a link like this:</p>
|
||||
<p>This is <a href="http://example.com/" title="Optional Title Here">an example</a> reference-style link.</p>
|
||||
<p>Elsewhere in the document, usually at the bottom of the file, you define your link label on a line by itself:</p>
|
||||
<pre><code>[id]: http://example.com/ "Optional Title Here"
|
||||
<pre><code>[id]: http://example.com/ "Optional Title Here"
|
||||
</code></pre>
|
||||
<p>Links can get pretty fancy, so if you want the long form version, visit the
|
||||
official <a href="http://daringfireball.net/projects/markdown/">Markdown</a> docs.</p>
|
||||
|
@ -84,7 +84,7 @@ ___triple underscores___
|
|||
<p><img src="http://www.addictedtoibiza.com/wp-content/uploads/2012/12/example.png" alt="Alt text" /></p>
|
||||
<pre><code>![Alt text](http://www.addictedtoibiza.com/wp-content/uploads/2012/12/example.png)
|
||||
|
||||
![Alt text](http://www.addictedtoibiza.com/wp-content/uploads/2012/12/example.png "Optional title")
|
||||
![Alt text](http://www.addictedtoibiza.com/wp-content/uploads/2012/12/example.png "Optional title")
|
||||
</code></pre>
|
||||
<hr />
|
||||
<h1 id="blockelements">Block Elements</h1>
|
||||
|
@ -157,7 +157,7 @@ This is a second.
|
|||
<li>This is the second list item.</li>
|
||||
</ol>
|
||||
<p>Here's some example code:</p>
|
||||
<pre><code>return shell_exec("echo $input | $markdown_script");
|
||||
<pre><code>return shell_exec("echo $input | $markdown_script");
|
||||
</code></pre>
|
||||
</blockquote>
|
||||
<pre><code>> ## This is a header.
|
||||
|
@ -167,7 +167,7 @@ This is a second.
|
|||
>
|
||||
> Here's some example code:
|
||||
>
|
||||
> return shell_exec("echo $input | $markdown_script");
|
||||
> return shell_exec("echo $input | $markdown_script");
|
||||
</code></pre>
|
||||
<h2 id="lists">Lists</h2>
|
||||
<p>Markdown supports ordered (numbered) and unordered (bulleted) lists. List markers typically start at the left margin, but may be indented by up to three spaces. List markers must be followed by one or more spaces or a tab.</p>
|
||||
|
@ -331,7 +331,7 @@ Pipe | $1 | eleven
|
|||
<p>You can also highlight snippets of text (Markdown uses the excellent <a href="http://pygments.org/">Pygments</a> library) to allow you to use code highlighting Here's an example of some Python code:</p>
|
||||
<pre><code>#!python
|
||||
#
|
||||
def wiki_rocks(text): formatter = lambda t: "funky"+t return formatter(text)
|
||||
def wiki_rocks(text): formatter = lambda t: "funky"+t return formatter(text)
|
||||
</code></pre>
|
||||
<p>To do this, do not indent the block. Start the block with <code>```</code> three ticks. Then, provide the comment with the type of syntax you are using. There is a <a href="http://pygments.org/docs/lexers/">the vast library of Pygment lexers</a>. Markdown accepts the 'short name' or the 'mimetype' of anything in there.</p>
|
||||
<p>You can also use a fence style for code.</p>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
foo
|
||||
|
||||
```javascript
|
||||
var s = "JavaScript syntax highlighting";
|
||||
var s = "JavaScript syntax highlighting";
|
||||
alert(s);
|
||||
```
|
||||
|
||||
|
@ -11,12 +11,12 @@ bar
|
|||
<p>this is a long paragraph</p>
|
||||
<p>this is another long paragraph</p>
|
||||
<pre lang="no-highlight"><code>```javascript
|
||||
var s = "JavaScript syntax highlighting";
|
||||
var s = "JavaScript syntax highlighting";
|
||||
alert(s);
|
||||
```
|
||||
|
||||
```python
|
||||
s = "Python syntax highlighting"
|
||||
s = "Python syntax highlighting"
|
||||
print s
|
||||
```
|
||||
</code></pre>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<pre lang="no-highlight"><code>
|
||||
```javascript
|
||||
var s = "JavaScript syntax highlighting";
|
||||
var s = "JavaScript syntax highlighting";
|
||||
alert(s);
|
||||
```
|
||||
|
||||
```python
|
||||
s = "Python syntax highlighting"
|
||||
s = "Python syntax highlighting"
|
||||
print s
|
||||
```
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
for (int i = 0; i < 10 && true; i++) {
|
||||
|
||||
System.out.println("Hello World");
|
||||
System.out.println("Hello World");
|
||||
}
|
||||
|
||||
// stuff here is affected as well <>&&%
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
for (int i = 0; i < 10 && true; i++) {
|
||||
|
||||
System.out.println("Hello World");
|
||||
System.out.println("Hello World");
|
||||
}
|
||||
|
||||
// stuff here is affected as well <>&&%
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<pre><code class="json language-json">{
|
||||
"custom": true
|
||||
"custom": true
|
||||
}
|
||||
</code></pre>
|
||||
<pre><code class="json language-json">{
|
||||
"custom": false
|
||||
"custom": false
|
||||
}
|
||||
</code></pre>
|
||||
|
|
|
@ -5,6 +5,6 @@
|
|||
<li>This is the second list item.</li>
|
||||
</ol>
|
||||
<p>Here's some example code:</p>
|
||||
<pre><code>return shell_exec("echo $input | $markdown_script");
|
||||
<pre><code>return shell_exec("echo $input | $markdown_script");
|
||||
</code></pre>
|
||||
</blockquote>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<ul>
|
||||
<li><p>list item 1</p>
|
||||
<pre><code class="html language-html"><a href="www.google.com">google</a>
|
||||
<pre><code class="html language-html"><a href="www.google.com">google</a>
|
||||
<div>
|
||||
<div>some div</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<p><code>some **code** yeah</code></p>
|
||||
<p>some <code>inline **code** block</code></p>
|
||||
<p><code>some inline **code**</code> block</p>
|
||||
<p>yo dawg <code start="true">some <code start="false">code</code> inception</code></p>
|
||||
<p>yo dawg <code start="true">some <code start="false">code</code> inception</code></p>
|
||||
<div>some **div** yeah</div>
|
|
@ -29,10 +29,6 @@ describe('makeHtml() commonmark testsuite', function () {
|
|||
//case 'Raw HTML_619': // breaks prettifier so the test fails
|
||||
continue;
|
||||
|
||||
case 'Setext headings_91': //it's failing because the testcase converts " to " even though it's not supposed to
|
||||
testsuite[section][i].expected = testsuite[section][i].expected.replace(/"/g, '"')
|
||||
break;
|
||||
|
||||
case 'Fenced code blocks_142': // we use different classes to mark languages in fenced code blocks
|
||||
case 'Fenced code blocks_143': // we use different classes to mark languages in fenced code blocks
|
||||
testsuite[section][i].expected = testsuite[section][i].expected.replace('language-ruby', 'ruby language-ruby');
|
||||
|
|
|
@ -109,16 +109,32 @@ describe('showdown.Event', function () {
|
|||
{ event: 'onHash', text: '```\nfoo\n```', result: true },
|
||||
{ event: 'onHash', text: 'foo', result: false }
|
||||
],
|
||||
heading: [
|
||||
'heading.atx': [
|
||||
{ event: 'onStart', text: '# foo', result: true },
|
||||
{ event: 'onStart', text: 'foo', result: true },
|
||||
{ event: 'onEnd', text: '# foo', result: true },
|
||||
{ event: 'onEnd', text: 'foo', result: true },
|
||||
{ event: 'onCapture', text: '# foo', result: true },
|
||||
{ event: 'onCapture', text: 'foo\n---', result: false },
|
||||
{ event: 'onCapture', text: 'foo\n===', result: false },
|
||||
{ event: 'onCapture', text: 'foo', result: false },
|
||||
{ event: 'onHash', text: '# foo', result: true },
|
||||
{ event: 'onHash', text: 'foo\n---', result: false },
|
||||
{ event: 'onHash', text: 'foo\n===', result: false },
|
||||
{ event: 'onHash', text: 'foo', result: false }
|
||||
],
|
||||
'heading.setext': [
|
||||
{ event: 'onStart', text: 'foo\n---', result: true },
|
||||
{ event: 'onStart', text: 'foo\n===', result: true },
|
||||
{ event: 'onStart', text: 'foo', result: true },
|
||||
{ event: 'onEnd', text: 'foo\n---', result: true },
|
||||
{ event: 'onEnd', text: 'foo\n===', result: true },
|
||||
{ event: 'onEnd', text: 'foo', result: true },
|
||||
{ event: 'onCapture', text: '# foo', result: false },
|
||||
{ event: 'onCapture', text: 'foo\n---', result: true },
|
||||
{ event: 'onCapture', text: 'foo\n===', result: true },
|
||||
{ event: 'onCapture', text: 'foo', result: false },
|
||||
{ event: 'onHash', text: '# foo', result: true },
|
||||
{ event: 'onHash', text: '# foo', result: false },
|
||||
{ event: 'onHash', text: 'foo\n---', result: true },
|
||||
{ event: 'onHash', text: 'foo\n===', result: true },
|
||||
{ event: 'onHash', text: 'foo', result: false }
|
||||
|
@ -267,8 +283,10 @@ describe('showdown.Event', function () {
|
|||
describe(parser, function () {
|
||||
for (let ts in testSpec.makehtml[parser]) {
|
||||
let event = 'makehtml.' + parser + '.' + testSpec.makehtml[parser][ts].event;
|
||||
|
||||
let md = testSpec.makehtml[parser][ts].text;
|
||||
let title = (testSpec.makehtml[parser][ts].result) ? 'should ' : 'should NOT ';
|
||||
let title = '«' + md + '» ';
|
||||
title += (testSpec.makehtml[parser][ts].result) ? 'should ' : 'should NOT ';
|
||||
title += 'trigger "' + event + ' event"';
|
||||
let expected = testSpec.makehtml[parser][ts].result;
|
||||
let actual = false;
|
||||
|
|
Loading…
Reference in New Issue