Premier commit déjà bien avancé

This commit is contained in:
2025-11-10 18:33:24 +01:00
commit db4f0508cb
652 changed files with 440521 additions and 0 deletions

View File

@ -0,0 +1,16 @@
name: Trigger CI
on: push
jobs:
build:
name: Dispatch to main repo
runs-on: ubuntu-latest
steps:
- name: Emit repository_dispatch
uses: mvasigh/dispatch-action@main
with:
# You should create a personal access token and store it in your repository
token: ${{ secrets.DISPATCH_AUTH }}
repo: dev
owner: codemirror
event_type: push

View File

@ -0,0 +1,246 @@
## 6.5.0 (2025-10-23)
### New features
Add a variant of `insertNewlineContinueMarkup` that supports configuration options.
## 6.4.0 (2025-10-02)
### New features
The new `pasteURLAsLink` extension allows you to paste URLs over a selection to quickly create a link.
## 6.3.4 (2025-08-01)
### Bug fixes
Make sure header-based indentation is available even when Markdown isn't the editor's top-level language.
## 6.3.3 (2025-06-13)
### Bug fixes
Make `insertNewlineContinueMarkup` take effect even when at the end of a nested range of Markdown content.
## 6.3.2 (2025-01-09)
### Bug fixes
Make Markdown-specific commands return false inside fenced code.
Fix an infinite loop caused by `insertNewlineContinueMarkup`.
## 6.3.1 (2024-11-06)
### Bug fixes
Fix an issue where `insertNewlineContinueMarkup` didn't work with the cursor directly after an HTML tag.
## 6.3.0 (2024-09-28)
### New features
The new `htmlTagLanguage` option allows client code to configure which language is used to parse HTML tags in the document.
## 6.2.5 (2024-04-12)
### Bug fixes
Disable folding for list nodes (since it will shadow the folding on the first list item).
## 6.2.4 (2024-01-16)
### Bug fixes
Starting at the third list item, `insertNewlineContinueMarkup` will now keep the tightness of the list, and only require two presses to clear an empty list item.
## 6.2.3 (2023-11-27)
### Bug fixes
Support code folding for GFM tables.
## 6.2.2 (2023-10-06)
### Bug fixes
Fix a bug in `insertNewlineContinueMarkup` that caused it to put the cursor in the wrong place when the editor's line break was more than one character long.
## 6.2.1 (2023-09-14)
### Bug fixes
Make `insertNewlineContinueMarkup` and `deleteMarkupBackward` use tabs for indentation when appropriate.
## 6.2.0 (2023-06-23)
### New features
The markdown package now installs a completion source that completes HTML tags when in Markdown context.
## 6.1.1 (2023-04-13)
### Bug fixes
Fix the declaration of `comentTokens` language data for Markdown.
Fix a bug in `deleteMarkupBackward` that would cause it to delete pieces of continued paragraphs below list item markers.
## 6.1.0 (2023-02-17)
### New features
Add support for folding entire sections from the header.
## 6.0.5 (2022-11-10)
### Bug fixes
Make sure task lists are indented correctly even when deeply nested.
## 6.0.4 (2022-11-02)
### Bug fixes
Fix an issue where nested task lists were indented too deeply.
## 6.0.3 (2022-10-24)
### Bug fixes
Add a `name` value to the Markdown language object.
## 6.0.2 (2022-10-10)
### Bug fixes
Improve `insertNewlineContinueMarkup`'s behavior in a fenced code block.
## 6.0.1 (2022-07-25)
### Bug fixes
Ignore text after whitespace in code block metadata, when determining which language the block is.
## 6.0.0 (2022-06-08)
### Breaking changes
Update dependencies to 6.0.0
## 0.20.1 (2022-05-20)
### New features
The `codeLanguages` option to `markdown` may now be a function from an info string to a language.
## 0.20.0 (2022-04-20)
### New features
`insertNewlineContinueMarkup` can now continue task lists. Move highlighting information into @lezer/markdown
## 0.19.6 (2022-02-04)
### Bug fixes
Fix an issue where `deleteMarkupBackward` could get confused when there was only whitespace between the cursor and the start of the line.
## 0.19.5 (2022-01-28)
### Bug fixes
Make `insertNewlineContinueMarkup` exit blockquotes after two blank lines.
## 0.19.4 (2022-01-03)
### Bug fixes
Fix a bug where list items after a removed item were incorrectly renumbered.
## 0.19.3 (2021-12-10)
### Bug fixes
`insertNewlineContinueMarkup` will no longer exit lists when there is content after the cursor.
Fix an issue in `deleteMarkupBackward` where it only deleted a single space when after a number marker.
## 0.19.2 (2021-10-20)
### Bug fixes
Fix a bug where the monospace highlighting tag wasn't correctly applied to code block content.
## 0.19.1 (2021-08-11)
### Bug fixes
Fix incorrect versions for @lezer dependencies.
## 0.19.0 (2021-08-11)
### Breaking changes
Update dependencies to 0.19.0
## 0.18.4 (2021-06-16)
### Bug fixes
Fix a case where `deleteMarkupBackward` would return true without actually having an effect.
## 0.18.3 (2021-05-19)
### Bug fixes
`insertNewlineContinueMarkup` will not continue moving list markers down when they are after an empty line anymore.
## 0.18.2 (2021-05-07)
### Bug fixes
Fix a bug where `insertNewlineContinueMarkup` could duplicate bits of content when in dededented continued list items.
## 0.18.1 (2021-04-01)
### Bug fixes
Add `monospace` style tag to all children of inline code nodes.
## 0.18.0 (2021-03-03)
### Breaking changes
Update dependencies to 0.18.
## 0.17.3 (2021-02-22)
### New features
Include heading depth in style tags.
## 0.17.2 (2021-02-10)
### Bug fixes
Fix a bug where `insertNewlineContinueMarkup` would sometimes duplicate bits of content.
### New features
The package now exports both a `commonmarkLanguage`, with just plain CommonMark, and a `markdownLanguage`, with GFM and some other extensions enabled.
It is now possible to pass lezer-markdown extensions to the `markdown` function to configure the parser.
## 0.17.1 (2021-01-06)
### New features
The package now also exports a CommonJS module.
## 0.17.0 (2020-12-29)
### Breaking changes
First numbered release.

View File

@ -0,0 +1,21 @@
MIT License
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,162 @@
<!-- NOTE: README.md is generated from src/README.md -->
# @codemirror/lang-markdown [![NPM version](https://img.shields.io/npm/v/@codemirror/lang-markdown.svg)](https://www.npmjs.org/package/@codemirror/lang-markdown)
[ [**WEBSITE**](https://codemirror.net/) | [**ISSUES**](https://github.com/codemirror/dev/issues) | [**FORUM**](https://discuss.codemirror.net/c/next/) | [**CHANGELOG**](https://github.com/codemirror/lang-markdown/blob/main/CHANGELOG.md) ]
This package implements Markdown language support for the
[CodeMirror](https://codemirror.net/) code editor.
The [project page](https://codemirror.net/) has more information, a
number of [examples](https://codemirror.net/examples/) and the
[documentation](https://codemirror.net/docs/).
This code is released under an
[MIT license](https://github.com/codemirror/lang-markdown/tree/main/LICENSE).
We aim to be an inclusive, welcoming community. To make that explicit,
we have a [code of
conduct](http://contributor-covenant.org/version/1/1/0/) that applies
to communication around the project.
## Usage
```javascript
import {EditorView, basicSetup} from "codemirror"
import {markdown} from "@codemirror/lang-markdown"
const view = new EditorView({
parent: document.body,
doc: `*CodeMirror* Markdown \`mode\``,
extensions: [basicSetup, markdown()]
})
```
## API Reference
<dl>
<dt id="user-content-markdown">
<code><strong><a href="#user-content-markdown">markdown</a></strong>(<a id="user-content-markdown^config" href="#user-content-markdown^config">config</a>&#8288;?: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object">Object</a> = {}) → <a href="https://codemirror.net/docs/ref#language.LanguageSupport">LanguageSupport</a></code></dt>
<dd><p>Markdown language support.</p>
<dl><dt id="user-content-markdown^config">
<code><strong><a href="#user-content-markdown^config">config</a></strong></code></dt>
<dd><dl><dt id="user-content-markdown^config.defaultcodelanguage">
<code><strong><a href="#user-content-markdown^config.defaultcodelanguage">defaultCodeLanguage</a></strong>&#8288;?: <a href="https://codemirror.net/docs/ref#language.Language">Language</a> | <a href="https://codemirror.net/docs/ref#language.LanguageSupport">LanguageSupport</a></code></dt>
<dd><p>When given, this language will be used by default to parse code
blocks.</p>
</dd><dt id="user-content-markdown^config.codelanguages">
<code><strong><a href="#user-content-markdown^config.codelanguages">codeLanguages</a></strong>&#8288;?: readonly <a href="https://codemirror.net/docs/ref#language.LanguageDescription">LanguageDescription</a>[] | fn(<a id="user-content-markdown^config.codelanguages^info" href="#user-content-markdown^config.codelanguages^info">info</a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String">string</a>) → <a href="https://codemirror.net/docs/ref#language.Language">Language</a> | <a href="https://codemirror.net/docs/ref#language.LanguageDescription">LanguageDescription</a> | <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/null">null</a></code></dt>
<dd><p>A source of language support for highlighting fenced code
blocks. When it is an array, the parser will use
<a href="https://codemirror.net/docs/ref/#language.LanguageDescription%5EmatchLanguageName"><code>LanguageDescription.matchLanguageName</code></a>
with the fenced code info to find a matching language. When it
is a function, will be called with the info string and may
return a language or <code>LanguageDescription</code> object.</p>
</dd><dt id="user-content-markdown^config.addkeymap">
<code><strong><a href="#user-content-markdown^config.addkeymap">addKeymap</a></strong>&#8288;?: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean">boolean</a></code></dt>
<dd><p>Set this to false to disable installation of the Markdown
<a href="#user-content-markdownkeymap">keymap</a>.</p>
</dd><dt id="user-content-markdown^config.extensions">
<code><strong><a href="#user-content-markdown^config.extensions">extensions</a></strong>&#8288;?: <a href="https://github.com/lezer-parser/markdown#user-content-markdownextension">MarkdownExtension</a></code></dt>
<dd><p>Markdown parser
<a href="https://github.com/lezer-parser/markdown#user-content-markdownextension">extensions</a>
to add to the parser.</p>
</dd><dt id="user-content-markdown^config.base">
<code><strong><a href="#user-content-markdown^config.base">base</a></strong>&#8288;?: <a href="https://codemirror.net/docs/ref#language.Language">Language</a></code></dt>
<dd><p>The base language to use. Defaults to
<a href="#user-content-commonmarklanguage"><code>commonmarkLanguage</code></a>.</p>
</dd><dt id="user-content-markdown^config.completehtmltags">
<code><strong><a href="#user-content-markdown^config.completehtmltags">completeHTMLTags</a></strong>&#8288;?: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean">boolean</a></code></dt>
<dd><p>By default, the extension installs an autocompletion source that
completes HTML tags when a <code>&lt;</code> is typed. Set this to false to
disable this.</p>
</dd><dt id="user-content-markdown^config.pasteurlaslink">
<code><strong><a href="#user-content-markdown^config.pasteurlaslink">pasteURLAsLink</a></strong>&#8288;?: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean">boolean</a></code></dt>
<dd><p>The returned language contains
<a href="#user-content-pasteurlaslink"><code>pasteURLAsLink</code></a> as a support
extension unless you set this to false.</p>
</dd><dt id="user-content-markdown^config.htmltaglanguage">
<code><strong><a href="#user-content-markdown^config.htmltaglanguage">htmlTagLanguage</a></strong>&#8288;?: <a href="https://codemirror.net/docs/ref#language.LanguageSupport">LanguageSupport</a></code></dt>
<dd><p>By default, HTML tags in the document are handled by the <a href="https://github.com/codemirror/lang-html">HTML
language</a> package with
tag matching turned off. You can pass in an alternative language
configuration here if you want.</p>
</dd></dl></dd></dl></dd>
<dt id="user-content-markdownlanguage">
<code><strong><a href="#user-content-markdownlanguage">markdownLanguage</a></strong>: <a href="https://codemirror.net/docs/ref#language.Language">Language</a></code></dt>
<dd><p>Language support for <a href="https://github.github.com/gfm/">GFM</a> plus
subscript, superscript, and emoji syntax.</p>
</dd>
<dt id="user-content-commonmarklanguage">
<code><strong><a href="#user-content-commonmarklanguage">commonmarkLanguage</a></strong>: <a href="https://codemirror.net/docs/ref#language.Language">Language</a></code></dt>
<dd><p>Language support for strict CommonMark.</p>
</dd>
<dt id="user-content-insertnewlinecontinuemarkup">
<code><strong><a href="#user-content-insertnewlinecontinuemarkup">insertNewlineContinueMarkup</a></strong>: <a href="https://codemirror.net/docs/ref#state.StateCommand">StateCommand</a></code></dt>
<dd><p>This command, when invoked in Markdown context with cursor
selection(s), will create a new line with the markup for
blockquotes and lists that were active on the old line. If the
cursor was directly after the end of the markup for the old line,
trailing whitespace and list markers are removed from that line.</p>
<p>The command does nothing in non-Markdown context, so it should
not be used as the only binding for Enter (even in a Markdown
document, HTML and code regions might use a different language).</p>
</dd>
<dt id="user-content-insertnewlinecontinuemarkupcommand">
<code><strong><a href="#user-content-insertnewlinecontinuemarkupcommand">insertNewlineContinueMarkupCommand</a></strong>(<a id="user-content-insertnewlinecontinuemarkupcommand^config" href="#user-content-insertnewlinecontinuemarkupcommand^config">config</a>&#8288;?: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object">Object</a> = {}) → <a href="https://codemirror.net/docs/ref#state.StateCommand">StateCommand</a></code></dt>
<dd><p>Returns a command like
<a href="#user-content-insertnewlinecontinuemarkup"><code>insertNewlineContinueMarkup</code></a>,
allowing further configuration.</p>
<dl><dt id="user-content-insertnewlinecontinuemarkupcommand^config">
<code><strong><a href="#user-content-insertnewlinecontinuemarkupcommand^config">config</a></strong></code></dt>
<dd><dl><dt id="user-content-insertnewlinecontinuemarkupcommand^config.nontightlists">
<code><strong><a href="#user-content-insertnewlinecontinuemarkupcommand^config.nontightlists">nonTightLists</a></strong>&#8288;?: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean">boolean</a></code></dt>
<dd><p>By default, when pressing enter in a blank second item in a
tight (no blank lines between items) list, the command will
insert a blank line above that item, starting a non-tight list.
Set this to false to disable this behavior.</p>
</dd></dl></dd></dl></dd>
<dt id="user-content-deletemarkupbackward">
<code><strong><a href="#user-content-deletemarkupbackward">deleteMarkupBackward</a></strong>: <a href="https://codemirror.net/docs/ref#state.StateCommand">StateCommand</a></code></dt>
<dd><p>This command will, when invoked in a Markdown context with the
cursor directly after list or blockquote markup, delete one level
of markup. When the markup is for a list, it will be replaced by
spaces on the first invocation (a further invocation will delete
the spaces), to make it easy to continue a list.</p>
<p>When not after Markdown block markup, this command will return
false, so it is intended to be bound alongside other deletion
commands, with a higher precedence than the more generic commands.</p>
</dd>
<dt id="user-content-markdownkeymap">
<code><strong><a href="#user-content-markdownkeymap">markdownKeymap</a></strong>: readonly <a href="https://codemirror.net/docs/ref#view.KeyBinding">KeyBinding</a>[]</code></dt>
<dd><p>A small keymap with Markdown-specific bindings. Binds Enter to
<a href="#user-content-insertnewlinecontinuemarkup"><code>insertNewlineContinueMarkup</code></a>
and Backspace to
<a href="#user-content-deletemarkupbackward"><code>deleteMarkupBackward</code></a>.</p>
</dd>
<dt id="user-content-pasteurlaslink">
<code><strong><a href="#user-content-pasteurlaslink">pasteURLAsLink</a></strong>: <a href="https://codemirror.net/docs/ref#state.Extension">Extension</a></code></dt>
<dd><p>An extension that intercepts pastes when the pasted content looks
like a URL and the selection is non-empty and selects regular
text, making the selection a link with the pasted URL as target.</p>
</dd>
</dl>

View File

@ -0,0 +1,501 @@
'use strict';
var state = require('@codemirror/state');
var view = require('@codemirror/view');
var language = require('@codemirror/language');
var autocomplete = require('@codemirror/autocomplete');
var markdown$1 = require('@lezer/markdown');
var langHtml = require('@codemirror/lang-html');
var common = require('@lezer/common');
const data = language.defineLanguageFacet({ commentTokens: { block: { open: "<!--", close: "-->" } } });
const headingProp = new common.NodeProp();
const commonmark = markdown$1.parser.configure({
props: [
language.foldNodeProp.add(type => {
return !type.is("Block") || type.is("Document") || isHeading(type) != null || isList(type) ? undefined
: (tree, state) => ({ from: state.doc.lineAt(tree.from).to, to: tree.to });
}),
headingProp.add(isHeading),
language.indentNodeProp.add({
Document: () => null
}),
language.languageDataProp.add({
Document: data
})
]
});
function isHeading(type) {
let match = /^(?:ATX|Setext)Heading(\d)$/.exec(type.name);
return match ? +match[1] : undefined;
}
function isList(type) {
return type.name == "OrderedList" || type.name == "BulletList";
}
function findSectionEnd(headerNode, level) {
let last = headerNode;
for (;;) {
let next = last.nextSibling, heading;
if (!next || (heading = isHeading(next.type)) != null && heading <= level)
break;
last = next;
}
return last.to;
}
const headerIndent = language.foldService.of((state, start, end) => {
for (let node = language.syntaxTree(state).resolveInner(end, -1); node; node = node.parent) {
if (node.from < start)
break;
let heading = node.type.prop(headingProp);
if (heading == null)
continue;
let upto = findSectionEnd(node, heading);
if (upto > end)
return { from: end, to: upto };
}
return null;
});
function mkLang(parser) {
return new language.Language(data, parser, [], "markdown");
}
/**
Language support for strict CommonMark.
*/
const commonmarkLanguage = mkLang(commonmark);
const extended = commonmark.configure([markdown$1.GFM, markdown$1.Subscript, markdown$1.Superscript, markdown$1.Emoji, {
props: [
language.foldNodeProp.add({
Table: (tree, state) => ({ from: state.doc.lineAt(tree.from).to, to: tree.to })
})
]
}]);
/**
Language support for [GFM](https://github.github.com/gfm/) plus
subscript, superscript, and emoji syntax.
*/
const markdownLanguage = mkLang(extended);
function getCodeParser(languages, defaultLanguage) {
return (info) => {
if (info && languages) {
let found = null;
// Strip anything after whitespace
info = /\S*/.exec(info)[0];
if (typeof languages == "function")
found = languages(info);
else
found = language.LanguageDescription.matchLanguageName(languages, info, true);
if (found instanceof language.LanguageDescription)
return found.support ? found.support.language.parser : language.ParseContext.getSkippingParser(found.load());
else if (found)
return found.parser;
}
return defaultLanguage ? defaultLanguage.parser : null;
};
}
class Context {
constructor(node, from, to, spaceBefore, spaceAfter, type, item) {
this.node = node;
this.from = from;
this.to = to;
this.spaceBefore = spaceBefore;
this.spaceAfter = spaceAfter;
this.type = type;
this.item = item;
}
blank(maxWidth, trailing = true) {
let result = this.spaceBefore + (this.node.name == "Blockquote" ? ">" : "");
if (maxWidth != null) {
while (result.length < maxWidth)
result += " ";
return result;
}
else {
for (let i = this.to - this.from - result.length - this.spaceAfter.length; i > 0; i--)
result += " ";
return result + (trailing ? this.spaceAfter : "");
}
}
marker(doc, add) {
let number = this.node.name == "OrderedList" ? String((+itemNumber(this.item, doc)[2] + add)) : "";
return this.spaceBefore + number + this.type + this.spaceAfter;
}
}
function getContext(node, doc) {
let nodes = [], context = [];
for (let cur = node; cur; cur = cur.parent) {
if (cur.name == "FencedCode")
return context;
if (cur.name == "ListItem" || cur.name == "Blockquote")
nodes.push(cur);
}
for (let i = nodes.length - 1; i >= 0; i--) {
let node = nodes[i], match;
let line = doc.lineAt(node.from), startPos = node.from - line.from;
if (node.name == "Blockquote" && (match = /^ *>( ?)/.exec(line.text.slice(startPos)))) {
context.push(new Context(node, startPos, startPos + match[0].length, "", match[1], ">", null));
}
else if (node.name == "ListItem" && node.parent.name == "OrderedList" &&
(match = /^( *)\d+([.)])( *)/.exec(line.text.slice(startPos)))) {
let after = match[3], len = match[0].length;
if (after.length >= 4) {
after = after.slice(0, after.length - 4);
len -= 4;
}
context.push(new Context(node.parent, startPos, startPos + len, match[1], after, match[2], node));
}
else if (node.name == "ListItem" && node.parent.name == "BulletList" &&
(match = /^( *)([-+*])( {1,4}\[[ xX]\])?( +)/.exec(line.text.slice(startPos)))) {
let after = match[4], len = match[0].length;
if (after.length > 4) {
after = after.slice(0, after.length - 4);
len -= 4;
}
let type = match[2];
if (match[3])
type += match[3].replace(/[xX]/, ' ');
context.push(new Context(node.parent, startPos, startPos + len, match[1], after, type, node));
}
}
return context;
}
function itemNumber(item, doc) {
return /^(\s*)(\d+)(?=[.)])/.exec(doc.sliceString(item.from, item.from + 10));
}
function renumberList(after, doc, changes, offset = 0) {
for (let prev = -1, node = after;;) {
if (node.name == "ListItem") {
let m = itemNumber(node, doc);
let number = +m[2];
if (prev >= 0) {
if (number != prev + 1)
return;
changes.push({ from: node.from + m[1].length, to: node.from + m[0].length, insert: String(prev + 2 + offset) });
}
prev = number;
}
let next = node.nextSibling;
if (!next)
break;
node = next;
}
}
function normalizeIndent(content, state$1) {
let blank = /^[ \t]*/.exec(content)[0].length;
if (!blank || state$1.facet(language.indentUnit) != "\t")
return content;
let col = state.countColumn(content, 4, blank);
let space = "";
for (let i = col; i > 0;) {
if (i >= 4) {
space += "\t";
i -= 4;
}
else {
space += " ";
i--;
}
}
return space + content.slice(blank);
}
/**
Returns a command like
[`insertNewlineContinueMarkup`](https://codemirror.net/6/docs/ref/#lang-markdown.insertNewlineContinueMarkup),
allowing further configuration.
*/
const insertNewlineContinueMarkupCommand = (config = {}) => ({ state: state$1, dispatch }) => {
let tree = language.syntaxTree(state$1), { doc } = state$1;
let dont = null, changes = state$1.changeByRange(range => {
if (!range.empty || !markdownLanguage.isActiveAt(state$1, range.from, -1) && !markdownLanguage.isActiveAt(state$1, range.from, 1))
return dont = { range };
let pos = range.from, line = doc.lineAt(pos);
let context = getContext(tree.resolveInner(pos, -1), doc);
while (context.length && context[context.length - 1].from > pos - line.from)
context.pop();
if (!context.length)
return dont = { range };
let inner = context[context.length - 1];
if (inner.to - inner.spaceAfter.length > pos - line.from)
return dont = { range };
let emptyLine = pos >= (inner.to - inner.spaceAfter.length) && !/\S/.test(line.text.slice(inner.to));
// Empty line in list
if (inner.item && emptyLine) {
let first = inner.node.firstChild, second = inner.node.getChild("ListItem", "ListItem");
// Not second item or blank line before: delete a level of markup
if (first.to >= pos || second && second.to < pos ||
line.from > 0 && !/[^\s>]/.test(doc.lineAt(line.from - 1).text) ||
config.nonTightLists === false) {
let next = context.length > 1 ? context[context.length - 2] : null;
let delTo, insert = "";
if (next && next.item) { // Re-add marker for the list at the next level
delTo = line.from + next.from;
insert = next.marker(doc, 1);
}
else {
delTo = line.from + (next ? next.to : 0);
}
let changes = [{ from: delTo, to: pos, insert }];
if (inner.node.name == "OrderedList")
renumberList(inner.item, doc, changes, -2);
if (next && next.node.name == "OrderedList")
renumberList(next.item, doc, changes);
return { range: state.EditorSelection.cursor(delTo + insert.length), changes };
}
else { // Move second item down, making tight two-item list non-tight
let insert = blankLine(context, state$1, line);
return { range: state.EditorSelection.cursor(pos + insert.length + 1),
changes: { from: line.from, insert: insert + state$1.lineBreak } };
}
}
if (inner.node.name == "Blockquote" && emptyLine && line.from) {
let prevLine = doc.lineAt(line.from - 1), quoted = />\s*$/.exec(prevLine.text);
// Two aligned empty quoted lines in a row
if (quoted && quoted.index == inner.from) {
let changes = state$1.changes([{ from: prevLine.from + quoted.index, to: prevLine.to },
{ from: line.from + inner.from, to: line.to }]);
return { range: range.map(changes), changes };
}
}
let changes = [];
if (inner.node.name == "OrderedList")
renumberList(inner.item, doc, changes);
let continued = inner.item && inner.item.from < line.from;
let insert = "";
// If not dedented
if (!continued || /^[\s\d.)\-+*>]*/.exec(line.text)[0].length >= inner.to) {
for (let i = 0, e = context.length - 1; i <= e; i++) {
insert += i == e && !continued ? context[i].marker(doc, 1)
: context[i].blank(i < e ? state.countColumn(line.text, 4, context[i + 1].from) - insert.length : null);
}
}
let from = pos;
while (from > line.from && /\s/.test(line.text.charAt(from - line.from - 1)))
from--;
insert = normalizeIndent(insert, state$1);
if (nonTightList(inner.node, state$1.doc))
insert = blankLine(context, state$1, line) + state$1.lineBreak + insert;
changes.push({ from, to: pos, insert: state$1.lineBreak + insert });
return { range: state.EditorSelection.cursor(from + insert.length + 1), changes };
});
if (dont)
return false;
dispatch(state$1.update(changes, { scrollIntoView: true, userEvent: "input" }));
return true;
};
/**
This command, when invoked in Markdown context with cursor
selection(s), will create a new line with the markup for
blockquotes and lists that were active on the old line. If the
cursor was directly after the end of the markup for the old line,
trailing whitespace and list markers are removed from that line.
The command does nothing in non-Markdown context, so it should
not be used as the only binding for Enter (even in a Markdown
document, HTML and code regions might use a different language).
*/
const insertNewlineContinueMarkup = insertNewlineContinueMarkupCommand();
function isMark(node) {
return node.name == "QuoteMark" || node.name == "ListMark";
}
function nonTightList(node, doc) {
if (node.name != "OrderedList" && node.name != "BulletList")
return false;
let first = node.firstChild, second = node.getChild("ListItem", "ListItem");
if (!second)
return false;
let line1 = doc.lineAt(first.to), line2 = doc.lineAt(second.from);
let empty = /^[\s>]*$/.test(line1.text);
return line1.number + (empty ? 0 : 1) < line2.number;
}
function blankLine(context, state$1, line) {
let insert = "";
for (let i = 0, e = context.length - 2; i <= e; i++) {
insert += context[i].blank(i < e
? state.countColumn(line.text, 4, context[i + 1].from) - insert.length
: null, i < e);
}
return normalizeIndent(insert, state$1);
}
function contextNodeForDelete(tree, pos) {
let node = tree.resolveInner(pos, -1), scan = pos;
if (isMark(node)) {
scan = node.from;
node = node.parent;
}
for (let prev; prev = node.childBefore(scan);) {
if (isMark(prev)) {
scan = prev.from;
}
else if (prev.name == "OrderedList" || prev.name == "BulletList") {
node = prev.lastChild;
scan = node.to;
}
else {
break;
}
}
return node;
}
/**
This command will, when invoked in a Markdown context with the
cursor directly after list or blockquote markup, delete one level
of markup. When the markup is for a list, it will be replaced by
spaces on the first invocation (a further invocation will delete
the spaces), to make it easy to continue a list.
When not after Markdown block markup, this command will return
false, so it is intended to be bound alongside other deletion
commands, with a higher precedence than the more generic commands.
*/
const deleteMarkupBackward = ({ state: state$1, dispatch }) => {
let tree = language.syntaxTree(state$1);
let dont = null, changes = state$1.changeByRange(range => {
let pos = range.from, { doc } = state$1;
if (range.empty && markdownLanguage.isActiveAt(state$1, range.from)) {
let line = doc.lineAt(pos);
let context = getContext(contextNodeForDelete(tree, pos), doc);
if (context.length) {
let inner = context[context.length - 1];
let spaceEnd = inner.to - inner.spaceAfter.length + (inner.spaceAfter ? 1 : 0);
// Delete extra trailing space after markup
if (pos - line.from > spaceEnd && !/\S/.test(line.text.slice(spaceEnd, pos - line.from)))
return { range: state.EditorSelection.cursor(line.from + spaceEnd),
changes: { from: line.from + spaceEnd, to: pos } };
if (pos - line.from == spaceEnd &&
// Only apply this if we're on the line that has the
// construct's syntax, or there's only indentation in the
// target range
(!inner.item || line.from <= inner.item.from || !/\S/.test(line.text.slice(0, inner.to)))) {
let start = line.from + inner.from;
// Replace a list item marker with blank space
if (inner.item && inner.node.from < inner.item.from && /\S/.test(line.text.slice(inner.from, inner.to))) {
let insert = inner.blank(state.countColumn(line.text, 4, inner.to) - state.countColumn(line.text, 4, inner.from));
if (start == line.from)
insert = normalizeIndent(insert, state$1);
return { range: state.EditorSelection.cursor(start + insert.length),
changes: { from: start, to: line.from + inner.to, insert } };
}
// Delete one level of indentation
if (start < pos)
return { range: state.EditorSelection.cursor(start), changes: { from: start, to: pos } };
}
}
}
return dont = { range };
});
if (dont)
return false;
dispatch(state$1.update(changes, { scrollIntoView: true, userEvent: "delete" }));
return true;
};
/**
A small keymap with Markdown-specific bindings. Binds Enter to
[`insertNewlineContinueMarkup`](https://codemirror.net/6/docs/ref/#lang-markdown.insertNewlineContinueMarkup)
and Backspace to
[`deleteMarkupBackward`](https://codemirror.net/6/docs/ref/#lang-markdown.deleteMarkupBackward).
*/
const markdownKeymap = [
{ key: "Enter", run: insertNewlineContinueMarkup },
{ key: "Backspace", run: deleteMarkupBackward }
];
const htmlNoMatch = langHtml.html({ matchClosingTags: false });
/**
Markdown language support.
*/
function markdown(config = {}) {
let { codeLanguages, defaultCodeLanguage, addKeymap = true, base: { parser } = commonmarkLanguage, completeHTMLTags = true, pasteURLAsLink: pasteURL = true, htmlTagLanguage = htmlNoMatch } = config;
if (!(parser instanceof markdown$1.MarkdownParser))
throw new RangeError("Base parser provided to `markdown` should be a Markdown parser");
let extensions = config.extensions ? [config.extensions] : [];
let support = [htmlTagLanguage.support, headerIndent], defaultCode;
if (pasteURL)
support.push(pasteURLAsLink);
if (defaultCodeLanguage instanceof language.LanguageSupport) {
support.push(defaultCodeLanguage.support);
defaultCode = defaultCodeLanguage.language;
}
else if (defaultCodeLanguage) {
defaultCode = defaultCodeLanguage;
}
let codeParser = codeLanguages || defaultCode ? getCodeParser(codeLanguages, defaultCode) : undefined;
extensions.push(markdown$1.parseCode({ codeParser, htmlParser: htmlTagLanguage.language.parser }));
if (addKeymap)
support.push(state.Prec.high(view.keymap.of(markdownKeymap)));
let lang = mkLang(parser.configure(extensions));
if (completeHTMLTags)
support.push(lang.data.of({ autocomplete: htmlTagCompletion }));
return new language.LanguageSupport(lang, support);
}
function htmlTagCompletion(context) {
let { state, pos } = context, m = /<[:\-\.\w\u00b7-\uffff]*$/.exec(state.sliceDoc(pos - 25, pos));
if (!m)
return null;
let tree = language.syntaxTree(state).resolveInner(pos, -1);
while (tree && !tree.type.isTop) {
if (tree.name == "CodeBlock" || tree.name == "FencedCode" || tree.name == "ProcessingInstructionBlock" ||
tree.name == "CommentBlock" || tree.name == "Link" || tree.name == "Image")
return null;
tree = tree.parent;
}
return {
from: pos - m[0].length, to: pos,
options: htmlTagCompletions(),
validFor: /^<[:\-\.\w\u00b7-\uffff]*$/
};
}
let _tagCompletions = null;
function htmlTagCompletions() {
if (_tagCompletions)
return _tagCompletions;
let result = langHtml.htmlCompletionSource(new autocomplete.CompletionContext(state.EditorState.create({ extensions: htmlNoMatch }), 0, true));
return _tagCompletions = result ? result.options : [];
}
const nonPlainText = /code|horizontalrule|html|link|comment|processing|escape|entity|image|mark|url/i;
/**
An extension that intercepts pastes when the pasted content looks
like a URL and the selection is non-empty and selects regular
text, making the selection a link with the pasted URL as target.
*/
const pasteURLAsLink = view.EditorView.domEventHandlers({
paste: (event, view) => {
var _a;
let { main } = view.state.selection;
if (main.empty)
return false;
let link = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.getData("text/plain");
if (!link || !/^(https?:\/\/|mailto:|xmpp:|www\.)/.test(link))
return false;
if (/^www\./.test(link))
link = "https://" + link;
if (!markdownLanguage.isActiveAt(view.state, main.from, 1))
return false;
let tree = language.syntaxTree(view.state), crossesNode = false;
// Verify that no nodes are started/ended between the selection
// points, and we're not inside any non-plain-text construct.
tree.iterate({
from: main.from, to: main.to,
enter: node => { if (node.from > main.from || nonPlainText.test(node.name))
crossesNode = true; },
leave: node => { if (node.to < main.to)
crossesNode = true; }
});
if (crossesNode)
return false;
view.dispatch({
changes: [{ from: main.from, insert: "[" }, { from: main.to, insert: `](${link})` }],
userEvent: "input.paste",
scrollIntoView: true
});
return true;
}
});
exports.commonmarkLanguage = commonmarkLanguage;
exports.deleteMarkupBackward = deleteMarkupBackward;
exports.insertNewlineContinueMarkup = insertNewlineContinueMarkup;
exports.insertNewlineContinueMarkupCommand = insertNewlineContinueMarkupCommand;
exports.markdown = markdown;
exports.markdownKeymap = markdownKeymap;
exports.markdownLanguage = markdownLanguage;
exports.pasteURLAsLink = pasteURLAsLink;

View File

@ -0,0 +1,124 @@
import * as _codemirror_state from '@codemirror/state';
import { StateCommand } from '@codemirror/state';
import { KeyBinding } from '@codemirror/view';
import { Language, LanguageSupport, LanguageDescription } from '@codemirror/language';
import { MarkdownExtension } from '@lezer/markdown';
/**
Language support for strict CommonMark.
*/
declare const commonmarkLanguage: Language;
/**
Language support for [GFM](https://github.github.com/gfm/) plus
subscript, superscript, and emoji syntax.
*/
declare const markdownLanguage: Language;
/**
Returns a command like
[`insertNewlineContinueMarkup`](https://codemirror.net/6/docs/ref/#lang-markdown.insertNewlineContinueMarkup),
allowing further configuration.
*/
declare const insertNewlineContinueMarkupCommand: (config?: {
/**
By default, when pressing enter in a blank second item in a
tight (no blank lines between items) list, the command will
insert a blank line above that item, starting a non-tight list.
Set this to false to disable this behavior.
*/
nonTightLists?: boolean;
}) => StateCommand;
/**
This command, when invoked in Markdown context with cursor
selection(s), will create a new line with the markup for
blockquotes and lists that were active on the old line. If the
cursor was directly after the end of the markup for the old line,
trailing whitespace and list markers are removed from that line.
The command does nothing in non-Markdown context, so it should
not be used as the only binding for Enter (even in a Markdown
document, HTML and code regions might use a different language).
*/
declare const insertNewlineContinueMarkup: StateCommand;
/**
This command will, when invoked in a Markdown context with the
cursor directly after list or blockquote markup, delete one level
of markup. When the markup is for a list, it will be replaced by
spaces on the first invocation (a further invocation will delete
the spaces), to make it easy to continue a list.
When not after Markdown block markup, this command will return
false, so it is intended to be bound alongside other deletion
commands, with a higher precedence than the more generic commands.
*/
declare const deleteMarkupBackward: StateCommand;
/**
A small keymap with Markdown-specific bindings. Binds Enter to
[`insertNewlineContinueMarkup`](https://codemirror.net/6/docs/ref/#lang-markdown.insertNewlineContinueMarkup)
and Backspace to
[`deleteMarkupBackward`](https://codemirror.net/6/docs/ref/#lang-markdown.deleteMarkupBackward).
*/
declare const markdownKeymap: readonly KeyBinding[];
/**
Markdown language support.
*/
declare function markdown(config?: {
/**
When given, this language will be used by default to parse code
blocks.
*/
defaultCodeLanguage?: Language | LanguageSupport;
/**
A source of language support for highlighting fenced code
blocks. When it is an array, the parser will use
[`LanguageDescription.matchLanguageName`](https://codemirror.net/6/docs/ref/#language.LanguageDescription^matchLanguageName)
with the fenced code info to find a matching language. When it
is a function, will be called with the info string and may
return a language or `LanguageDescription` object.
*/
codeLanguages?: readonly LanguageDescription[] | ((info: string) => Language | LanguageDescription | null);
/**
Set this to false to disable installation of the Markdown
[keymap](https://codemirror.net/6/docs/ref/#lang-markdown.markdownKeymap).
*/
addKeymap?: boolean;
/**
Markdown parser
[extensions](https://github.com/lezer-parser/markdown#user-content-markdownextension)
to add to the parser.
*/
extensions?: MarkdownExtension;
/**
The base language to use. Defaults to
[`commonmarkLanguage`](https://codemirror.net/6/docs/ref/#lang-markdown.commonmarkLanguage).
*/
base?: Language;
/**
By default, the extension installs an autocompletion source that
completes HTML tags when a `<` is typed. Set this to false to
disable this.
*/
completeHTMLTags?: boolean;
/**
The returned language contains
[`pasteURLAsLink`](https://codemirror.net/6/docs/ref/#lang-markdown.pasteURLAsLink) as a support
extension unless you set this to false.
*/
pasteURLAsLink?: boolean;
/**
By default, HTML tags in the document are handled by the [HTML
language](https://github.com/codemirror/lang-html) package with
tag matching turned off. You can pass in an alternative language
configuration here if you want.
*/
htmlTagLanguage?: LanguageSupport;
}): LanguageSupport;
/**
An extension that intercepts pastes when the pasted content looks
like a URL and the selection is non-empty and selects regular
text, making the selection a link with the pasted URL as target.
*/
declare const pasteURLAsLink: _codemirror_state.Extension;
export { commonmarkLanguage, deleteMarkupBackward, insertNewlineContinueMarkup, insertNewlineContinueMarkupCommand, markdown, markdownKeymap, markdownLanguage, pasteURLAsLink };

View File

@ -0,0 +1,124 @@
import * as _codemirror_state from '@codemirror/state';
import { StateCommand } from '@codemirror/state';
import { KeyBinding } from '@codemirror/view';
import { Language, LanguageSupport, LanguageDescription } from '@codemirror/language';
import { MarkdownExtension } from '@lezer/markdown';
/**
Language support for strict CommonMark.
*/
declare const commonmarkLanguage: Language;
/**
Language support for [GFM](https://github.github.com/gfm/) plus
subscript, superscript, and emoji syntax.
*/
declare const markdownLanguage: Language;
/**
Returns a command like
[`insertNewlineContinueMarkup`](https://codemirror.net/6/docs/ref/#lang-markdown.insertNewlineContinueMarkup),
allowing further configuration.
*/
declare const insertNewlineContinueMarkupCommand: (config?: {
/**
By default, when pressing enter in a blank second item in a
tight (no blank lines between items) list, the command will
insert a blank line above that item, starting a non-tight list.
Set this to false to disable this behavior.
*/
nonTightLists?: boolean;
}) => StateCommand;
/**
This command, when invoked in Markdown context with cursor
selection(s), will create a new line with the markup for
blockquotes and lists that were active on the old line. If the
cursor was directly after the end of the markup for the old line,
trailing whitespace and list markers are removed from that line.
The command does nothing in non-Markdown context, so it should
not be used as the only binding for Enter (even in a Markdown
document, HTML and code regions might use a different language).
*/
declare const insertNewlineContinueMarkup: StateCommand;
/**
This command will, when invoked in a Markdown context with the
cursor directly after list or blockquote markup, delete one level
of markup. When the markup is for a list, it will be replaced by
spaces on the first invocation (a further invocation will delete
the spaces), to make it easy to continue a list.
When not after Markdown block markup, this command will return
false, so it is intended to be bound alongside other deletion
commands, with a higher precedence than the more generic commands.
*/
declare const deleteMarkupBackward: StateCommand;
/**
A small keymap with Markdown-specific bindings. Binds Enter to
[`insertNewlineContinueMarkup`](https://codemirror.net/6/docs/ref/#lang-markdown.insertNewlineContinueMarkup)
and Backspace to
[`deleteMarkupBackward`](https://codemirror.net/6/docs/ref/#lang-markdown.deleteMarkupBackward).
*/
declare const markdownKeymap: readonly KeyBinding[];
/**
Markdown language support.
*/
declare function markdown(config?: {
/**
When given, this language will be used by default to parse code
blocks.
*/
defaultCodeLanguage?: Language | LanguageSupport;
/**
A source of language support for highlighting fenced code
blocks. When it is an array, the parser will use
[`LanguageDescription.matchLanguageName`](https://codemirror.net/6/docs/ref/#language.LanguageDescription^matchLanguageName)
with the fenced code info to find a matching language. When it
is a function, will be called with the info string and may
return a language or `LanguageDescription` object.
*/
codeLanguages?: readonly LanguageDescription[] | ((info: string) => Language | LanguageDescription | null);
/**
Set this to false to disable installation of the Markdown
[keymap](https://codemirror.net/6/docs/ref/#lang-markdown.markdownKeymap).
*/
addKeymap?: boolean;
/**
Markdown parser
[extensions](https://github.com/lezer-parser/markdown#user-content-markdownextension)
to add to the parser.
*/
extensions?: MarkdownExtension;
/**
The base language to use. Defaults to
[`commonmarkLanguage`](https://codemirror.net/6/docs/ref/#lang-markdown.commonmarkLanguage).
*/
base?: Language;
/**
By default, the extension installs an autocompletion source that
completes HTML tags when a `<` is typed. Set this to false to
disable this.
*/
completeHTMLTags?: boolean;
/**
The returned language contains
[`pasteURLAsLink`](https://codemirror.net/6/docs/ref/#lang-markdown.pasteURLAsLink) as a support
extension unless you set this to false.
*/
pasteURLAsLink?: boolean;
/**
By default, HTML tags in the document are handled by the [HTML
language](https://github.com/codemirror/lang-html) package with
tag matching turned off. You can pass in an alternative language
configuration here if you want.
*/
htmlTagLanguage?: LanguageSupport;
}): LanguageSupport;
/**
An extension that intercepts pastes when the pasted content looks
like a URL and the selection is non-empty and selects regular
text, making the selection a link with the pasted URL as target.
*/
declare const pasteURLAsLink: _codemirror_state.Extension;
export { commonmarkLanguage, deleteMarkupBackward, insertNewlineContinueMarkup, insertNewlineContinueMarkupCommand, markdown, markdownKeymap, markdownLanguage, pasteURLAsLink };

View File

@ -0,0 +1,492 @@
import { EditorSelection, countColumn, Prec, EditorState } from '@codemirror/state';
import { EditorView, keymap } from '@codemirror/view';
import { defineLanguageFacet, foldNodeProp, indentNodeProp, languageDataProp, foldService, syntaxTree, Language, LanguageDescription, ParseContext, indentUnit, LanguageSupport } from '@codemirror/language';
import { CompletionContext } from '@codemirror/autocomplete';
import { parser, GFM, Subscript, Superscript, Emoji, MarkdownParser, parseCode } from '@lezer/markdown';
import { html, htmlCompletionSource } from '@codemirror/lang-html';
import { NodeProp } from '@lezer/common';
const data = /*@__PURE__*/defineLanguageFacet({ commentTokens: { block: { open: "<!--", close: "-->" } } });
const headingProp = /*@__PURE__*/new NodeProp();
const commonmark = /*@__PURE__*/parser.configure({
props: [
/*@__PURE__*/foldNodeProp.add(type => {
return !type.is("Block") || type.is("Document") || isHeading(type) != null || isList(type) ? undefined
: (tree, state) => ({ from: state.doc.lineAt(tree.from).to, to: tree.to });
}),
/*@__PURE__*/headingProp.add(isHeading),
/*@__PURE__*/indentNodeProp.add({
Document: () => null
}),
/*@__PURE__*/languageDataProp.add({
Document: data
})
]
});
function isHeading(type) {
let match = /^(?:ATX|Setext)Heading(\d)$/.exec(type.name);
return match ? +match[1] : undefined;
}
function isList(type) {
return type.name == "OrderedList" || type.name == "BulletList";
}
function findSectionEnd(headerNode, level) {
let last = headerNode;
for (;;) {
let next = last.nextSibling, heading;
if (!next || (heading = isHeading(next.type)) != null && heading <= level)
break;
last = next;
}
return last.to;
}
const headerIndent = /*@__PURE__*/foldService.of((state, start, end) => {
for (let node = syntaxTree(state).resolveInner(end, -1); node; node = node.parent) {
if (node.from < start)
break;
let heading = node.type.prop(headingProp);
if (heading == null)
continue;
let upto = findSectionEnd(node, heading);
if (upto > end)
return { from: end, to: upto };
}
return null;
});
function mkLang(parser) {
return new Language(data, parser, [], "markdown");
}
/**
Language support for strict CommonMark.
*/
const commonmarkLanguage = /*@__PURE__*/mkLang(commonmark);
const extended = /*@__PURE__*/commonmark.configure([GFM, Subscript, Superscript, Emoji, {
props: [
/*@__PURE__*/foldNodeProp.add({
Table: (tree, state) => ({ from: state.doc.lineAt(tree.from).to, to: tree.to })
})
]
}]);
/**
Language support for [GFM](https://github.github.com/gfm/) plus
subscript, superscript, and emoji syntax.
*/
const markdownLanguage = /*@__PURE__*/mkLang(extended);
function getCodeParser(languages, defaultLanguage) {
return (info) => {
if (info && languages) {
let found = null;
// Strip anything after whitespace
info = /\S*/.exec(info)[0];
if (typeof languages == "function")
found = languages(info);
else
found = LanguageDescription.matchLanguageName(languages, info, true);
if (found instanceof LanguageDescription)
return found.support ? found.support.language.parser : ParseContext.getSkippingParser(found.load());
else if (found)
return found.parser;
}
return defaultLanguage ? defaultLanguage.parser : null;
};
}
class Context {
constructor(node, from, to, spaceBefore, spaceAfter, type, item) {
this.node = node;
this.from = from;
this.to = to;
this.spaceBefore = spaceBefore;
this.spaceAfter = spaceAfter;
this.type = type;
this.item = item;
}
blank(maxWidth, trailing = true) {
let result = this.spaceBefore + (this.node.name == "Blockquote" ? ">" : "");
if (maxWidth != null) {
while (result.length < maxWidth)
result += " ";
return result;
}
else {
for (let i = this.to - this.from - result.length - this.spaceAfter.length; i > 0; i--)
result += " ";
return result + (trailing ? this.spaceAfter : "");
}
}
marker(doc, add) {
let number = this.node.name == "OrderedList" ? String((+itemNumber(this.item, doc)[2] + add)) : "";
return this.spaceBefore + number + this.type + this.spaceAfter;
}
}
function getContext(node, doc) {
let nodes = [], context = [];
for (let cur = node; cur; cur = cur.parent) {
if (cur.name == "FencedCode")
return context;
if (cur.name == "ListItem" || cur.name == "Blockquote")
nodes.push(cur);
}
for (let i = nodes.length - 1; i >= 0; i--) {
let node = nodes[i], match;
let line = doc.lineAt(node.from), startPos = node.from - line.from;
if (node.name == "Blockquote" && (match = /^ *>( ?)/.exec(line.text.slice(startPos)))) {
context.push(new Context(node, startPos, startPos + match[0].length, "", match[1], ">", null));
}
else if (node.name == "ListItem" && node.parent.name == "OrderedList" &&
(match = /^( *)\d+([.)])( *)/.exec(line.text.slice(startPos)))) {
let after = match[3], len = match[0].length;
if (after.length >= 4) {
after = after.slice(0, after.length - 4);
len -= 4;
}
context.push(new Context(node.parent, startPos, startPos + len, match[1], after, match[2], node));
}
else if (node.name == "ListItem" && node.parent.name == "BulletList" &&
(match = /^( *)([-+*])( {1,4}\[[ xX]\])?( +)/.exec(line.text.slice(startPos)))) {
let after = match[4], len = match[0].length;
if (after.length > 4) {
after = after.slice(0, after.length - 4);
len -= 4;
}
let type = match[2];
if (match[3])
type += match[3].replace(/[xX]/, ' ');
context.push(new Context(node.parent, startPos, startPos + len, match[1], after, type, node));
}
}
return context;
}
function itemNumber(item, doc) {
return /^(\s*)(\d+)(?=[.)])/.exec(doc.sliceString(item.from, item.from + 10));
}
function renumberList(after, doc, changes, offset = 0) {
for (let prev = -1, node = after;;) {
if (node.name == "ListItem") {
let m = itemNumber(node, doc);
let number = +m[2];
if (prev >= 0) {
if (number != prev + 1)
return;
changes.push({ from: node.from + m[1].length, to: node.from + m[0].length, insert: String(prev + 2 + offset) });
}
prev = number;
}
let next = node.nextSibling;
if (!next)
break;
node = next;
}
}
function normalizeIndent(content, state) {
let blank = /^[ \t]*/.exec(content)[0].length;
if (!blank || state.facet(indentUnit) != "\t")
return content;
let col = countColumn(content, 4, blank);
let space = "";
for (let i = col; i > 0;) {
if (i >= 4) {
space += "\t";
i -= 4;
}
else {
space += " ";
i--;
}
}
return space + content.slice(blank);
}
/**
Returns a command like
[`insertNewlineContinueMarkup`](https://codemirror.net/6/docs/ref/#lang-markdown.insertNewlineContinueMarkup),
allowing further configuration.
*/
const insertNewlineContinueMarkupCommand = (config = {}) => ({ state, dispatch }) => {
let tree = syntaxTree(state), { doc } = state;
let dont = null, changes = state.changeByRange(range => {
if (!range.empty || !markdownLanguage.isActiveAt(state, range.from, -1) && !markdownLanguage.isActiveAt(state, range.from, 1))
return dont = { range };
let pos = range.from, line = doc.lineAt(pos);
let context = getContext(tree.resolveInner(pos, -1), doc);
while (context.length && context[context.length - 1].from > pos - line.from)
context.pop();
if (!context.length)
return dont = { range };
let inner = context[context.length - 1];
if (inner.to - inner.spaceAfter.length > pos - line.from)
return dont = { range };
let emptyLine = pos >= (inner.to - inner.spaceAfter.length) && !/\S/.test(line.text.slice(inner.to));
// Empty line in list
if (inner.item && emptyLine) {
let first = inner.node.firstChild, second = inner.node.getChild("ListItem", "ListItem");
// Not second item or blank line before: delete a level of markup
if (first.to >= pos || second && second.to < pos ||
line.from > 0 && !/[^\s>]/.test(doc.lineAt(line.from - 1).text) ||
config.nonTightLists === false) {
let next = context.length > 1 ? context[context.length - 2] : null;
let delTo, insert = "";
if (next && next.item) { // Re-add marker for the list at the next level
delTo = line.from + next.from;
insert = next.marker(doc, 1);
}
else {
delTo = line.from + (next ? next.to : 0);
}
let changes = [{ from: delTo, to: pos, insert }];
if (inner.node.name == "OrderedList")
renumberList(inner.item, doc, changes, -2);
if (next && next.node.name == "OrderedList")
renumberList(next.item, doc, changes);
return { range: EditorSelection.cursor(delTo + insert.length), changes };
}
else { // Move second item down, making tight two-item list non-tight
let insert = blankLine(context, state, line);
return { range: EditorSelection.cursor(pos + insert.length + 1),
changes: { from: line.from, insert: insert + state.lineBreak } };
}
}
if (inner.node.name == "Blockquote" && emptyLine && line.from) {
let prevLine = doc.lineAt(line.from - 1), quoted = />\s*$/.exec(prevLine.text);
// Two aligned empty quoted lines in a row
if (quoted && quoted.index == inner.from) {
let changes = state.changes([{ from: prevLine.from + quoted.index, to: prevLine.to },
{ from: line.from + inner.from, to: line.to }]);
return { range: range.map(changes), changes };
}
}
let changes = [];
if (inner.node.name == "OrderedList")
renumberList(inner.item, doc, changes);
let continued = inner.item && inner.item.from < line.from;
let insert = "";
// If not dedented
if (!continued || /^[\s\d.)\-+*>]*/.exec(line.text)[0].length >= inner.to) {
for (let i = 0, e = context.length - 1; i <= e; i++) {
insert += i == e && !continued ? context[i].marker(doc, 1)
: context[i].blank(i < e ? countColumn(line.text, 4, context[i + 1].from) - insert.length : null);
}
}
let from = pos;
while (from > line.from && /\s/.test(line.text.charAt(from - line.from - 1)))
from--;
insert = normalizeIndent(insert, state);
if (nonTightList(inner.node, state.doc))
insert = blankLine(context, state, line) + state.lineBreak + insert;
changes.push({ from, to: pos, insert: state.lineBreak + insert });
return { range: EditorSelection.cursor(from + insert.length + 1), changes };
});
if (dont)
return false;
dispatch(state.update(changes, { scrollIntoView: true, userEvent: "input" }));
return true;
};
/**
This command, when invoked in Markdown context with cursor
selection(s), will create a new line with the markup for
blockquotes and lists that were active on the old line. If the
cursor was directly after the end of the markup for the old line,
trailing whitespace and list markers are removed from that line.
The command does nothing in non-Markdown context, so it should
not be used as the only binding for Enter (even in a Markdown
document, HTML and code regions might use a different language).
*/
const insertNewlineContinueMarkup = /*@__PURE__*/insertNewlineContinueMarkupCommand();
function isMark(node) {
return node.name == "QuoteMark" || node.name == "ListMark";
}
function nonTightList(node, doc) {
if (node.name != "OrderedList" && node.name != "BulletList")
return false;
let first = node.firstChild, second = node.getChild("ListItem", "ListItem");
if (!second)
return false;
let line1 = doc.lineAt(first.to), line2 = doc.lineAt(second.from);
let empty = /^[\s>]*$/.test(line1.text);
return line1.number + (empty ? 0 : 1) < line2.number;
}
function blankLine(context, state, line) {
let insert = "";
for (let i = 0, e = context.length - 2; i <= e; i++) {
insert += context[i].blank(i < e
? countColumn(line.text, 4, context[i + 1].from) - insert.length
: null, i < e);
}
return normalizeIndent(insert, state);
}
function contextNodeForDelete(tree, pos) {
let node = tree.resolveInner(pos, -1), scan = pos;
if (isMark(node)) {
scan = node.from;
node = node.parent;
}
for (let prev; prev = node.childBefore(scan);) {
if (isMark(prev)) {
scan = prev.from;
}
else if (prev.name == "OrderedList" || prev.name == "BulletList") {
node = prev.lastChild;
scan = node.to;
}
else {
break;
}
}
return node;
}
/**
This command will, when invoked in a Markdown context with the
cursor directly after list or blockquote markup, delete one level
of markup. When the markup is for a list, it will be replaced by
spaces on the first invocation (a further invocation will delete
the spaces), to make it easy to continue a list.
When not after Markdown block markup, this command will return
false, so it is intended to be bound alongside other deletion
commands, with a higher precedence than the more generic commands.
*/
const deleteMarkupBackward = ({ state, dispatch }) => {
let tree = syntaxTree(state);
let dont = null, changes = state.changeByRange(range => {
let pos = range.from, { doc } = state;
if (range.empty && markdownLanguage.isActiveAt(state, range.from)) {
let line = doc.lineAt(pos);
let context = getContext(contextNodeForDelete(tree, pos), doc);
if (context.length) {
let inner = context[context.length - 1];
let spaceEnd = inner.to - inner.spaceAfter.length + (inner.spaceAfter ? 1 : 0);
// Delete extra trailing space after markup
if (pos - line.from > spaceEnd && !/\S/.test(line.text.slice(spaceEnd, pos - line.from)))
return { range: EditorSelection.cursor(line.from + spaceEnd),
changes: { from: line.from + spaceEnd, to: pos } };
if (pos - line.from == spaceEnd &&
// Only apply this if we're on the line that has the
// construct's syntax, or there's only indentation in the
// target range
(!inner.item || line.from <= inner.item.from || !/\S/.test(line.text.slice(0, inner.to)))) {
let start = line.from + inner.from;
// Replace a list item marker with blank space
if (inner.item && inner.node.from < inner.item.from && /\S/.test(line.text.slice(inner.from, inner.to))) {
let insert = inner.blank(countColumn(line.text, 4, inner.to) - countColumn(line.text, 4, inner.from));
if (start == line.from)
insert = normalizeIndent(insert, state);
return { range: EditorSelection.cursor(start + insert.length),
changes: { from: start, to: line.from + inner.to, insert } };
}
// Delete one level of indentation
if (start < pos)
return { range: EditorSelection.cursor(start), changes: { from: start, to: pos } };
}
}
}
return dont = { range };
});
if (dont)
return false;
dispatch(state.update(changes, { scrollIntoView: true, userEvent: "delete" }));
return true;
};
/**
A small keymap with Markdown-specific bindings. Binds Enter to
[`insertNewlineContinueMarkup`](https://codemirror.net/6/docs/ref/#lang-markdown.insertNewlineContinueMarkup)
and Backspace to
[`deleteMarkupBackward`](https://codemirror.net/6/docs/ref/#lang-markdown.deleteMarkupBackward).
*/
const markdownKeymap = [
{ key: "Enter", run: insertNewlineContinueMarkup },
{ key: "Backspace", run: deleteMarkupBackward }
];
const htmlNoMatch = /*@__PURE__*/html({ matchClosingTags: false });
/**
Markdown language support.
*/
function markdown(config = {}) {
let { codeLanguages, defaultCodeLanguage, addKeymap = true, base: { parser } = commonmarkLanguage, completeHTMLTags = true, pasteURLAsLink: pasteURL = true, htmlTagLanguage = htmlNoMatch } = config;
if (!(parser instanceof MarkdownParser))
throw new RangeError("Base parser provided to `markdown` should be a Markdown parser");
let extensions = config.extensions ? [config.extensions] : [];
let support = [htmlTagLanguage.support, headerIndent], defaultCode;
if (pasteURL)
support.push(pasteURLAsLink);
if (defaultCodeLanguage instanceof LanguageSupport) {
support.push(defaultCodeLanguage.support);
defaultCode = defaultCodeLanguage.language;
}
else if (defaultCodeLanguage) {
defaultCode = defaultCodeLanguage;
}
let codeParser = codeLanguages || defaultCode ? getCodeParser(codeLanguages, defaultCode) : undefined;
extensions.push(parseCode({ codeParser, htmlParser: htmlTagLanguage.language.parser }));
if (addKeymap)
support.push(Prec.high(keymap.of(markdownKeymap)));
let lang = mkLang(parser.configure(extensions));
if (completeHTMLTags)
support.push(lang.data.of({ autocomplete: htmlTagCompletion }));
return new LanguageSupport(lang, support);
}
function htmlTagCompletion(context) {
let { state, pos } = context, m = /<[:\-\.\w\u00b7-\uffff]*$/.exec(state.sliceDoc(pos - 25, pos));
if (!m)
return null;
let tree = syntaxTree(state).resolveInner(pos, -1);
while (tree && !tree.type.isTop) {
if (tree.name == "CodeBlock" || tree.name == "FencedCode" || tree.name == "ProcessingInstructionBlock" ||
tree.name == "CommentBlock" || tree.name == "Link" || tree.name == "Image")
return null;
tree = tree.parent;
}
return {
from: pos - m[0].length, to: pos,
options: htmlTagCompletions(),
validFor: /^<[:\-\.\w\u00b7-\uffff]*$/
};
}
let _tagCompletions = null;
function htmlTagCompletions() {
if (_tagCompletions)
return _tagCompletions;
let result = htmlCompletionSource(new CompletionContext(EditorState.create({ extensions: htmlNoMatch }), 0, true));
return _tagCompletions = result ? result.options : [];
}
const nonPlainText = /code|horizontalrule|html|link|comment|processing|escape|entity|image|mark|url/i;
/**
An extension that intercepts pastes when the pasted content looks
like a URL and the selection is non-empty and selects regular
text, making the selection a link with the pasted URL as target.
*/
const pasteURLAsLink = /*@__PURE__*/EditorView.domEventHandlers({
paste: (event, view) => {
var _a;
let { main } = view.state.selection;
if (main.empty)
return false;
let link = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.getData("text/plain");
if (!link || !/^(https?:\/\/|mailto:|xmpp:|www\.)/.test(link))
return false;
if (/^www\./.test(link))
link = "https://" + link;
if (!markdownLanguage.isActiveAt(view.state, main.from, 1))
return false;
let tree = syntaxTree(view.state), crossesNode = false;
// Verify that no nodes are started/ended between the selection
// points, and we're not inside any non-plain-text construct.
tree.iterate({
from: main.from, to: main.to,
enter: node => { if (node.from > main.from || nonPlainText.test(node.name))
crossesNode = true; },
leave: node => { if (node.to < main.to)
crossesNode = true; }
});
if (crossesNode)
return false;
view.dispatch({
changes: [{ from: main.from, insert: "[" }, { from: main.to, insert: `](${link})` }],
userEvent: "input.paste",
scrollIntoView: true
});
return true;
}
});
export { commonmarkLanguage, deleteMarkupBackward, insertNewlineContinueMarkup, insertNewlineContinueMarkupCommand, markdown, markdownKeymap, markdownLanguage, pasteURLAsLink };

View File

@ -0,0 +1,44 @@
{
"name": "@codemirror/lang-markdown",
"version": "6.5.0",
"description": "Markdown language support for the CodeMirror code editor",
"scripts": {
"test": "cm-runtests",
"prepare": "cm-buildhelper src/index.ts"
},
"keywords": [
"editor",
"code"
],
"author": {
"name": "Marijn Haverbeke",
"email": "marijn@haverbeke.berlin",
"url": "http://marijnhaverbeke.nl"
},
"type": "module",
"main": "dist/index.cjs",
"exports": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"types": "dist/index.d.ts",
"module": "dist/index.js",
"sideEffects": false,
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.7.1",
"@codemirror/lang-html": "^6.0.0",
"@codemirror/language": "^6.3.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/markdown": "^1.0.0",
"@lezer/common": "^1.2.1"
},
"devDependencies": {
"@codemirror/buildhelper": "^1.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/codemirror/lang-markdown.git"
}
}