prepare for 3.0.0

这个提交包含在:
bhsd 2024-02-06 13:10:37 +08:00
父节点 8be93a703f
当前提交 8242ab64c5
共有 19 个文件被更改,包括 180 次插入2404 次删除

查看文件

@ -626,6 +626,7 @@
1,
{
"exemptedBy": [
"license",
"type"
],
"checkConstructors": false,

查看文件

@ -14,10 +14,12 @@ name: "CodeQL"
on:
push:
paths:
- '*.js'
- '**/*.ts'
- '!**/*.d.ts'
pull_request:
paths:
- '*.js'
- '**/*.ts'
- '!**/*.d.ts'
jobs:
analyze:

5
.gitignore vendored
查看文件

@ -1,3 +1,4 @@
/dist/
/typings/
/.vscode/
/dist/
/.markdownlint.json
.eslintcache

查看文件

@ -1,4 +1,5 @@
# Wikiplus-highlight
[![npm version](https://badge.fury.io/js/wikiplus-highlight.svg)](https://www.npmjs.com/package/wikiplus-highlight)
[![CodeQL](https://github.com/bhsd-harry/Wikiplus-highlight/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/bhsd-harry/Wikiplus-highlight/actions/workflows/github-code-scanning/codeql)
@ -17,7 +18,7 @@ mw.loader.load('//cdn.jsdelivr.net/npm/wikiplus-highlight@2.60.3');
```javascript
mw.loader.load('//unpkg.com/wikiplus-highlight@2.60.3/dist/main.min.js');
mw.loader.load('//unpkg.com/wikiplus-highlight@2.60.3/dist/main.js');
```
### 稳定版本
@ -31,45 +32,26 @@ mw.loader.load('//cdn.jsdelivr.net/npm/wikiplus-highlight');
```javascript
mw.loader.load('//unpkg.com/wikiplus-highlight/dist/main.min.js');
mw.loader.load('//unpkg.com/wikiplus-highlight/dist/main.js');
```
由于 CDN 的缓存机制,稳定版本的更新大约会滞后一周。
## 更多插件
使用 *[Vector](https://www.mediawiki.org/wiki/Skin:Vector)* 皮肤或 MediaWiki 1.35 及以上的 *[Minerva Neue](https://www.mediawiki.org/wiki/Skin:Minerva_Neue)* 皮肤时,在右上角的【更多】菜单将添加一个【Wikiplus高亮设置】选项,点击后可以选择想要加载的 CodeMirror 插件。其他桌面版皮肤(包括 *[Timeless](https://www.mediawiki.org/wiki/Skin:Timeless)*、*[MonoBook](https://www.mediawiki.org/wiki/Skin:MonoBook)* 和 *[Citizen](https://www.mediawiki.org/wiki/Skin:Citizen)*)会在类似性质的菜单中插入这一选项。默认加载【搜索】插件。修改插件设置后,不需要重载页面,只需打开一个新的 Wikiplus 编辑框就会立刻生效。
### 搜索
这个插件会向 Wikiplus 编辑区添加一个【搜索】按钮和一个【全文替换】按钮及以下快捷键:
- `Ctrl`/`Cmd` + `F`: 打开搜索框
- `Ctrl`/`Cmd` + `G`: 搜索下一处
- `Shift` + `Ctrl`/`Cmd` + `G`: 搜索上一处
- `Ctrl`/`Cmd` + `H`: 打开搜索框: 打开全文替换
- `Shift` + `Ctrl`/`Cmd` + `H`: 替换上一处
- `Ctrl`/`Cmd` + `Alt` + `Enter`: 开始全文替换
除了使用以上快捷键,也可以通过在搜索框中按下 `Enter` 键或点击搜索框右侧的 `▼` 按钮来执行搜索下一处的命令,点击 `▲` 按钮将执行搜索上一处的命令;在全文替换文本框中按下 `Enter` 键将执行全文替换命令,点击全文替换右侧的 `▼` 按钮执行替换下一处的命令,点击 `▲` 按钮将执行替换上一处的命令。搜索可以使用字符串,也可以使用形如 `/re/``/re/i` 的正则表达式。正则表达式仅接受可选的 `i` 修饰符,并默认添加 `m` 修饰符,暂不接受其他修饰符。 **注意:使用先行或后行断言时可能无法逐处替换。** 使用全文中匹配的字符都会添加淡粉色背景。如果没有匹配的结果,搜索框会添加粉色背景。
使用 *[Vector](https://www.mediawiki.org/wiki/Skin:Vector)* 皮肤或 MediaWiki 1.35 及以上的 *[Minerva Neue](https://www.mediawiki.org/wiki/Skin:Minerva_Neue)* 皮肤时,在右上角的【更多】菜单将添加一个【CodeMirror插件】选项,点击后可以选择想要加载的 CodeMirror 插件。其他桌面版皮肤(包括 *[Timeless](https://www.mediawiki.org/wiki/Skin:Timeless)*、*[MonoBook](https://www.mediawiki.org/wiki/Skin:MonoBook)* 和 *[Citizen](https://www.mediawiki.org/wiki/Skin:Citizen)*)会在类似性质的菜单中插入这一选项。修改插件设置后将立刻生效。
### 高亮当前行
使用这一插件时,如果当前没有选中的文字,光标所在行将添加浅蓝色背景。
使用这一插件时,光标所在行将添加浅蓝色背景。
### 尾随空格
使用这一插件时,每一行的尾随空格将添加红色波浪下划线
使用这一插件时,每一行的尾随空格将添加浅红色背景。
### 匹配括号
与 MediaWiki 官方提供的 [CodeMirror](https://www.mediawiki.org/wiki/Extension:CodeMirror) 扩展不同,这一插件只会在光标位于括号(包括 `{}`、`[]`,JavaScript、CSS 和 Lua 模式下还包括 `()`)处时才会生效,但括号匹配时会考虑其语法含义。以下是一个简单的例子:
```wikitext
<!--[-->]
```
CodeMirror 扩展会错误地匹配 `<!-- -->` 注释内外的括号,而这个插件不会。匹配的括号对会添加绿色边框,未匹配的单个括号会添加黄色边框。
匹配的括号对会添加绿色边框,未匹配的单个括号会添加黄色边框。
### 自动闭合括号和引号
@ -81,42 +63,31 @@ MediaWiki 模式下自动闭合生效的字符包括 `(`、`[`、`{` 和 `"`,
### 代码折叠
光标移动至模板参数或扩展/HTML 标签内部时会在该行代码上方出现一个淡黄色背景的 `` 标记,点击即可折叠模板参数或标签内部文本。折叠后使用一个带有虚线边框的 `⋯` 标记占位,点击该标记将重新展开折叠的代码部分。
### 添加 WikiEditor 工具条
工具条具备的功能遵从当前网站安装的 [WikiEditor](https://www.mediawiki.org/wiki/Extension:WikiEditor) 扩展的版本。
光标移动至模板内部时会在该行代码上方出现一个淡黄色背景的 `` 标记,点击即可折叠模板参数。折叠后使用一个带有虚线边框的 `⋯` 标记占位,点击该标记将重新展开折叠的代码部分。
### HTML/URL编码快捷键
添加以下快捷键:
- `Ctrl`/`Cmd` + `/`: 将选中的文字转换为HTML实体
- `Ctrl`/`Cmd` + `\`: 将选中的文字解码或转换为URL编码
- `Ctrl`/`Cmd` + `[`: 将选中的文字转换为HTML实体
- `Ctrl`/`Cmd` + `]`: 将选中的文字解码或转换为URL编码
### 快速打开模板
### 快速打开内部链接
右键点击被高亮标记为模板标题`#invoke` 魔术字的第一个参数(即模块标题)时,会在新标签页打开对应页面。如果当前站点安装了 [Widget](https://www.mediawiki.org/wiki/Extension:Widget) 扩展,则当右键点击 `#widget` 魔术字的第一个参数(即小部件标题)时会在新标签页打开对应小部件的编辑界面(即 `action=edit`)。
右键点击被高亮标记为内部链接或模板标题时,会在新标签页打开对应页面。
### 维基语法检查
这个功能基于 [wikiparser-node](https://github.com/bhsd-harry/wikiparser-node)。开启后将在编辑框内提示可能存在的维基语法错误,同时在右侧的滚动条上也会以红黄条纹粗略地指示错误在文中出现的位置。这个插件还将添加以下快捷键:
- `Ctrl`/`Cmd` + `L`: 随时开启或关闭错误提示
- `Ctrl`/`Cmd` + `K`: 立即刷新语法错误
这个功能基于 [wikiparser-node](https://github.com/bhsd-harry/wikiparser-node)。开启后将在编辑框内提示可能存在的维基语法错误。
### 调整缩进
这不是一个真正意义上的插件,而是一个设置选项。默认设置下使用 Tab 进行缩进。勾选这一选项时,会出现一个新的文本框以设置缩进对应的空格个数,初始值为 `4`
### 用于其他编辑器
勾选这一选项时,除[【搜索】](#搜索)和[【添加 WikiEditor 工具条】](#添加-wikieditor-工具条)插件以外的当前正在使用的插件也将作用于其他开启了 CodeMirror 高亮的编辑器。目前支持的编辑器包括 [InPageEdit-v2](https://github.com/inpageedit/inpageedit-v2) 和 [Inspector](https://zh.moegirl.org.cn/User:Bhsd/Inspector),不包括 MediaWiki 默认的编辑器。
这是一个设置选项。默认设置下使用 Tab 进行缩进。
## Supported languages
- **English (en)**
- **中文(简体) (zh-hans)**
- 中文(繁體) (zh-hant)
- English (en)
- 中文(简体) (zh-hans)
- 中文(繁體) (zh-hant)
[GNU General Public License 3.0](https://www.gnu.org/licenses/gpl-3.0-standalone.html)

16
bump.sh
查看文件

@ -1,23 +1,19 @@
#!/usr/local/bin/bash
if [[ $2 == 'npm' ]]
then
sed -i '' 's|gh/bhsd-harry/Wikiplus-highlight@|npm/wikiplus-highlight@|' main.js
uglifyjs main.js -c -m --comments --webkit --source-map 'url=main.min.js.map,root="../"' -o dist/main.min.js
sed -i '' 's|gh/bhsd-harry/Wikiplus-highlight|npm/wikiplus-highlight|' src/main.ts
npm run build
perl -pi -e "s|wikiplus-highlight@\d+\..+?(['/])|wikiplus-highlight\@$1\$1|" README.md
sed -i '' -E "s/\"version\": \".+\"/\"version\": \"$1\"/" package.json
git add -A
git commit -m "chore: publish $1 to npm"
npm publish
npm publish --tag ${3-latest}
else
eslint . && stylelint styles.css
npm run lint
if [[ $? -eq 0 ]]
then
sed -i '' -E "s/version = '.+'/version = '$1'/" main.js
sed -i '' 's|npm/wikiplus-highlight@|gh/bhsd-harry/Wikiplus-highlight@|' main.js
for file in i18n/*
do
sed -i '' -E "s/\"wphl-version\": \".+\"/\"wphl-version\": \"$1\"/" $file
done
sed -i '' -E "s/version = '.+'/version = '$1'/" src/main.ts
sed -i '' 's|npm/wikiplus-highlight|gh/bhsd-harry/Wikiplus-highlight|' src/main.ts
git add -A
git commit -m "chore: bump version to $1"
git push

264
fold.js
查看文件

@ -1,264 +0,0 @@
/**
* @author Bhsd <https://github.com/bhsd-harry>
* @license GPL-3.0
*/
(() => {
'use strict';
const {Pos, cmpPos, Init} = CodeMirror;
/**
* 只用于`title`属性的消息不存在时fallback到键名
* @param {string} key 消息键
* @param {string|undefined} argKey 额外参数的消息键
* @returns {string}
*/
const msg = (key, argKey) => {
const fullKey = `wphl-${key}`,
message = (argKey === undefined ? mw.msg(fullKey) : mw.msg(fullKey, msg(argKey)))
.replace(/&lt;/gu, '<').replace(/&gt;/gu, '>');
return message === `${fullKey}` ? key : message;
};
const braceRegex = /\bmw-template-bracket\b/u,
$placeholder = $('<span>', {text: '\u22ef', class: 'CodeMirror-widget-unfold'}),
$delimiter = $('<span>', {text: '|', class: 'cm-mw-template-delimiter'}),
$tt = $('<div>', {class: 'CodeMirror-tooltip', text: '\uff0d'}).click(
/** @this {HTMLDivElement} */
function() {
const /** @type {CodeMirror.FoldData} */ {cm, from, to, type} = $(this).fadeOut('fast').data(),
notTag = type === 'template' || type === 'comment',
$clonedPlaceholder = $placeholder.clone()
.attr('title', msg('unfold', notTag ? `fold-${type}` : `<${type}>`)),
mark = cm.markText(from, to, {
replacedWith: type === 'template'
? $('<span>', {html: [$delimiter.clone(), $clonedPlaceholder]})[0]
: $clonedPlaceholder[0],
selectLeft: type === 'template',
selectRight: false,
_isFold: true,
});
$clonedPlaceholder.click(() => {
mark.clear();
});
},
);
/**
* 隐藏tooltip
* @param {JQuery<HTMLElement>} $tooltip tooltip
*/
const hideTooltip = $tooltip => {
let timeout = -1,
executeTime = 0;
return /** @param {number} wait */ (wait, update = true) => {
if (update || executeTime - Date.now() > wait) {
clearTimeout(timeout);
timeout = setTimeout(() => {
$tooltip.fadeOut('fast');
timeout = -1;
}, wait);
executeTime = Date.now() + wait;
}
};
};
/**
* 搜索括号
* @param {CodeMirror.Editor} cm CodeMirror实例
* @param {CodeMirror.Position} where 当前位置
* @param {1|-1} dir 搜索方向
* @returns {CodeMirror.MarkerRange}
*/
const scanForDelimiterAndBracket = (cm, where, dir) => {
const maxScanLen = 10000,
maxScanLines = 1000,
{line, ch} = where,
lineEnd = dir > 0
? Math.min(cm.lastLine() + 1, line + maxScanLines)
: Math.max(cm.firstLine() - 1, line - maxScanLines);
let stack = 0,
hasDelimiter = dir < 0,
/** @type {CodeMirror.Position} */ delimiter;
for (let {line: l} = where; l !== lineEnd; l += dir) {
const curLine = cm.getLine(l);
if (!curLine) {
continue;
}
const {length} = curLine;
if (length > maxScanLen) {
continue;
}
const end = dir > 0 ? length : -1;
let pos = dir > 0 ? 0 : length - 1;
if (l === line) {
pos = ch - (dir > 0 ? 0 : 1); // `dir = 1`时不包含当前字符,`dir = -1`时包含当前字符
}
for (; pos !== end; pos += dir) {
const char = curLine.charAt(pos);
if (!hasDelimiter && /[^\s|]/u.test(char)) {
delimiter = Pos(l, pos + 1);
}
if (!(hasDelimiter ? /[{}]/u : /[{}|]/u).test(char)) {
continue;
}
const type = cm.getTokenTypeAt(Pos(l, pos + 1)) || '';
if (char === '|' && stack === 0 && /\bmw-template-delimiter\b/u.test(type)) {
hasDelimiter = true;
} else if (char === '|' || !braceRegex.test(type)) {
continue;
} else if (dir > 0 && char === '{' || dir < 0 && char === '}') {
stack++;
} else if (stack > 0) {
stack--;
} else {
return {from: hasDelimiter && delimiter, to: Pos(l, pos + (dir > 0 ? 0 : 1))};
}
}
}
return {};
};
/**
* 搜索模板
* @param {CodeMirror.Editor} cm CodeMirror实例
* @param {CodeMirror.Position} cursor 当前位置
* @returns {CodeMirror.MarkerRange}
*/
const findEnclosingTemplate = (cm, cursor) => {
const type = cm.getTokenTypeAt(cursor) || '';
if (!/\bmw-template\d*(?:-ext\d*)?(?:-link\d*)?-ground\b/u.test(type)
|| /\bmw-template-(?:bracket|name)\b/u.test(type)
) {
return undefined;
}
const {to: bracket} = scanForDelimiterAndBracket(cm, cursor, -1);
if (bracket) {
const {from, to} = scanForDelimiterAndBracket(cm, bracket, 1);
return typeof from === 'object' && (from.line < to.line || from.ch < to.ch - 2) ? {from, to} : undefined;
}
return undefined;
};
/**
* 搜索注释
* @param {CodeMirror.Editor} cm CodeMirror实例
* @param {CodeMirror.Position} cursor 当前位置
* @returns {CodeMirror.MarkerRange|undefined}
*/
const findEnclosingComment = (cm, cursor) => {
const {type, string, start, end} = cm.getTokenAt(cursor),
{ch} = cursor;
if (!/\bmw-comment\b/u.test(type)
|| string.startsWith('<!--') && ch <= start + 4
|| string.endsWith('-->') && ch >= end - 3
) {
return undefined;
}
const index = cm.indexFromPos(cursor),
text = cm.getValue(),
fromIndex = text.slice(0, index - 1).search(/<!--(?:(?!-->).)*$/su);
let toIndex = text.slice(index).indexOf('-->');
toIndex = toIndex === -1 ? text.length : toIndex + index;
return {from: cm.posFromIndex(fromIndex + 4), to: cm.posFromIndex(toIndex)};
};
/**
* 显示tooltip
* @param {CodeMirror.EditorFoldable} cm CodeMirror实例
*/
const showTooltip = cm => {
const {state: {fold: {$tooltip, hide}}} = cm;
cm.operation(() => {
if (cm.somethingSelected()) {
hide(500, false);
return;
}
const cursor = cm.getCursor();
let range = findEnclosingComment(cm, cursor),
type = 'comment';
if (!range) {
const template = findEnclosingTemplate(cm, cursor);
let tags = cm.findEnclosingTag(cursor);
if (tags && cmpPos(tags.open.to, tags.close.from) === 0) {
tags = undefined;
}
if (!template && !tags) {
hide(500, false);
return;
} else if (!tags || template && cmpPos(template.from, tags.open.to) > 0) {
range = template;
type = 'template';
} else {
range = {from: tags.open.to, to: tags.close.from};
type = tags.open.tag;
}
}
const {top, left} = cm.charCoords(cursor, 'local'),
height = $tooltip.outerHeight(),
notTag = type === 'template' || type === 'comment';
$tooltip.attr('title', msg('fold', notTag ? `fold-${type}` : `<${type}>`))
.toggleClass('cm-mw-htmltag-name', !notTag)
.toggleClass('cm-mw-template-name', type === 'template')
.toggleClass('cm-mw-comment', type === 'comment')
.css({top: top > height ? top - height : top + 17, left})
.data({...range, type})
.show();
hide(5000);
});
};
CodeMirror.defineExtension(
'scanForDelimiterAndBracket',
/**
* @this {CodeMirror.Editor}
* @param {CodeMirror.Position} where
* @param {1|-1} dir
*/
function(where, dir) {
return scanForDelimiterAndBracket(this, where || this.getCursor(), dir || 1);
},
);
CodeMirror.defineExtension(
'findEnclosingTemplate',
/**
* @this {CodeMirror.Editor}
* @param {CodeMirror.Position} pos
*/
function(pos) {
return findEnclosingTemplate(this, pos || this.getCursor());
},
);
CodeMirror.defineExtension(
'findEnclosingComment',
/**
* @this {CodeMirror.Editor}
* @param {CodeMirror.Position} pos
*/
function(pos) {
return findEnclosingComment(this, pos || this.getCursor());
},
);
CodeMirror.defineOption('fold', false, (/** @type {CodeMirror.EditorFoldable} */ cm, val, old) => {
if (old && old !== Init) {
cm.off('cursorActivity', showTooltip);
if (cm.state.fold) {
cm.state.fold.$tooltip.remove();
}
}
if (val) {
const $tooltip = $tt.clone(true).data('cm', cm).hide().appendTo(
$(cm.getScrollerElement()).children('.CodeMirror-sizer'),
);
cm.state.fold = {$tooltip, hide: hideTooltip($tooltip)};
cm.on('cursorActivity', showTooltip);
}
});
})();

查看文件

@ -1,35 +0,0 @@
{
"wphl-version": "2.60.3",
"wphl-lang": "en",
"wphl-addon-search": "<span title=\"Search with a string or a regular expression\">Search</span>",
"wphl-addon-activeline": "Show the active line",
"wphl-addon-trailingspace": "Show trailing spaces",
"wphl-addon-matchbrackets": "Highlight matching/nonmatching brackets",
"wphl-addon-closebrackets": "Auto-close brackets and quotes",
"wphl-addon-matchtags": "Highlight matching/nonmatching tags",
"wphl-addon-fold": "Fold template arguments and tags' innerHTML",
"wphl-addon-wikieditor": "Add WikiEditor toolbar",
"wphl-addon-escape": "<span title=\"Ctrl/Cmd + / : HTML encoding\nCtrl/Cmd + \\ : URL decoding or encoding\">Keyboard shortcuts for HTML/URL encoding</span>",
"wphl-addon-contextmenu": "Open the template or module page when right clicking",
"wphl-addon-indentwithspace": "Indent JavaScript/CSS/Lua codes with spaces instead of tabs",
"wphl-addon-indent": "Indent",
"wphl-addon-othereditors": "Apply selected addons to other editors",
"wphl-addon-lint": "Wikitext linter (Switch on/off by <code>Ctrl/Cmd + L</code>)",
"wphl-addon-label": "Please select the addons you wish to load",
"wphl-addon-notice": "Changes will apply when opening a new Wikiplus dialog.",
"wphl-addon-title": "Wikiplus Highlight Addons",
"wphl-search-placeholder": "Search using a string or a regex pattern",
"wphl-replace-placeholder": "string used in replaceAll",
"wphl-search-replace": "replaceAll",
"wphl-replace-count": "The pattern occurs $1 time(s). Do you want to replace them all?",
"wphl-unfold": "Click here to unfold $1",
"wphl-fold": "Click here to fold $1",
"wphl-fold-template": "template arguments",
"wphl-fold-comment": "HTML comment",
"wphl-contentmodel": "Please choose the content model:",
"wphl-feedback": "Bug reports or suggestions are welcome at <a href=\"https://github.com/bhsd-harry/Wikiplus-highlight/issues\" target=\"_blank\">GitHub</a> .",
"wphl-portlet": "Wikiplus Highlight",
"wphl-welcome": "Welcome to <b>Wikiplus-highlight</b>!<br>You can change your local settings <a id=\"wphl-settings-notify\" href=\"#\">here</a>.",
"wphl-welcome-upgrade": "<b>Wikiplus-highlight</b> has been upgraded to $1.<br>You may need to check and update your local settings <a id=\"wphl-settings-notify\" href=\"#\">here</a>.",
"wphl-welcome-new-addon": "<b>Wikiplus-highlight</b> has been upgraded to $1.<br>This version introduces $2 new {{plural:$2|addon|addons}}.<br>You may need to check and update your local settings <a id=\"wphl-settings-notify\" href=\"#\">here</a>."
}

查看文件

@ -1,35 +0,0 @@
{
"wphl-version": "2.60.3",
"wphl-lang": "ka",
"wphl-addon-search": "<span title=\"ძებნა ნიშნების მწკრივით ან რეგულარული გაფართოებით\">ძებნა</span>",
"wphl-addon-activeline": "აქტიური ხაზის მონიშვნა",
"wphl-addon-trailingspace": "საბოლოო შორისების მონიშვნა",
"wphl-addon-matchbrackets": "შესაბამისი/შეუსაბამო ფრჩხილების განათება",
"wphl-addon-closebrackets": "ფრჩხილებისა და ბრჭყალების ავტომატურად დახურვა",
"wphl-addon-matchtags": "შესაბამისი/შეუსაბამო ტეგების განათება",
"wphl-addon-fold": "თარგის არგუმენტებისა და HTML-ის ტეგების შეკეცვა",
"wphl-addon-wikieditor": "ვიკირედაქტორის (WikiEditor) ხელსაწყოთა პანელის დამატება",
"wphl-addon-escape": "<span title=\"Ctrl/Cmd + / : HTML-კოდირება\nCtrl/Cmd + \\ : URL-კოდირება\">კლავიატურის მალსახმობები HTML/URL-ის კოდირებისათვის</span>",
"wphl-addon-contextmenu": "თარგის ან მოდულის გვერდის გახსნა მარჯვენა ღილაკზე დაჭერისას",
"wphl-addon-indentwithspace": "JavaScript/CSS/Lua-ის კოდებში ტაბულაციის ნაცვლად შორისებით შეწევა",
"wphl-addon-indent": "შეწევა",
"wphl-addon-othereditors": "არჩეული დანამატების გამოყენება სხვა მომხმარებლებისთვისაც",
"wphl-addon-lint": "Wikitext linter (Switch on/off by <code>Ctrl/Cmd + L</code>)",
"wphl-addon-label": "აირჩიეთ ის დანამატები, რომელთა გამოყენებაც გსურთ",
"wphl-addon-notice": "ცვლილებები აისახება Wikiplus-ის ახალი დიალოგის გახსნისას.",
"wphl-addon-title": "Wikiplus-ის განათების დანამატები",
"wphl-search-placeholder": "ძებნა ნიშნების მწკრივის ან რეგულარული გაფართოების ნიმუშის გამოყენებით",
"wphl-replace-placeholder": "string used in replaceAll",
"wphl-search-replace": "replaceAll",
"wphl-replace-count": "The pattern occurs $1 time(s). Do you want to replace them all?",
"wphl-unfold": "აქ დააწკაპუნეთ, რათა $1 გაიშალოს",
"wphl-fold": "აქ დააწკაპუნეთ, რათა $1 შეიკეცოს",
"wphl-fold-template": "თარგის არგუმენტები",
"wphl-fold-comment": "HTML-ის კომენტარი",
"wphl-contentmodel": "გთხოვთ, აირჩიოთ შინაარსის მოდელი:",
"wphl-feedback": "ხარვეზების ან შემოთავაზებების შესახებ მოგვწერეთ <a href=\"https://github.com/bhsd-harry/Wikiplus-highlight/issues\" target=\"_blank\">GitHub</a>-ზე.",
"wphl-portlet": "Wikiplus-ის განათება",
"wphl-welcome": "მოგესალმებათ <b>Wikiplus-ის განათება</b>!<br>თქვენი ადგილობრივი პარამეტრების შეცვლა შეგიძლიათ <a id=\"wphl-settings-notify\" href=\"#\">აქ</a>.",
"wphl-welcome-upgrade": "<b>Wikiplus-ის განათება</b> გაუმჯობესდა $1 ვერსიამდე.<br>თქვენ შესაძლოა ადგილობრივი პარამეტრების ნახვა და განახლება დაგჭირდეთ (იხ. <a id=\"wphl-settings-notify\" href=\"#\">აქ</a>).",
"wphl-welcome-new-addon": "<b>Wikiplus-ის განათება</b> გაუმჯობესდა $1 ვერსიამდე.<br>ამ ვერსიამ შემოიტანა $2 ახალი დანამატი.<br>თქვენ შესაძლოა ადგილობრივი პარამეტრების ნახვა და განახლება დაგჭირდეთ (იხ. <a id=\"wphl-settings-notify\" href=\"#\">აქ</a>)."
}

查看文件

@ -1,35 +0,0 @@
{
"wphl-version": "2.60.3",
"wphl-lang": "zh-hans",
"wphl-addon-search": "<span title=\"搜索字符串或正则表达式\">搜索</span>",
"wphl-addon-activeline": "高亮显示光标所在行",
"wphl-addon-trailingspace": "显示尾随空格",
"wphl-addon-matchbrackets": "高亮显示匹配和未匹配的括号",
"wphl-addon-closebrackets": "自动闭合括号和引号",
"wphl-addon-matchtags": "高亮显示匹配和未匹配的标签",
"wphl-addon-fold": "折叠模板参数和标签内部文本",
"wphl-addon-wikieditor": "添加WikiEditor工具条",
"wphl-addon-escape": "<span title=\"Ctrl/Cmd + / : HTML编码\nCtrl/Cmd + \\ : URL解码或编码\">添加HTML和URL编码快捷键</span>",
"wphl-addon-contextmenu": "右键点击可打开模板或模块页面",
"wphl-addon-indentwithspace": "代码缩进使用空格而非Tab",
"wphl-addon-indent": "缩进",
"wphl-addon-othereditors": "将选中的插件用于其他编辑器",
"wphl-addon-lint": "维基语法检查(使用 <code>Ctrl/Cmd + L</code> 开关)",
"wphl-addon-label": "请选择您希望加载的插件",
"wphl-addon-notice": "更改将于打开新的Wikiplus编辑区时生效。",
"wphl-addon-title": "Wikiplus高亮插件",
"wphl-search-placeholder": "使用字符串或正则表达式搜索",
"wphl-replace-placeholder": "用于全文替换的字符串",
"wphl-search-replace": "全文替换",
"wphl-replace-count": "全文共有 $1 处待替换,确认替换吗?",
"wphl-unfold": "点击以展开$1",
"wphl-fold": "点击以折叠$1",
"wphl-fold-template": "模板参数",
"wphl-fold-comment": "HTML注释",
"wphl-contentmodel": "请选择内容模型:",
"wphl-feedback": "欢迎将错误报告或是改进建议提交到 <a href=\"https://github.com/bhsd-harry/Wikiplus-highlight/issues\" target=\"_blank\">GitHub</a> ",
"wphl-portlet": "Wikiplus高亮设置",
"wphl-welcome": "欢迎使用 <b>Wikiplus-highlight</b><br>您可以点击 <a id=\"wphl-settings-notify\" href=\"#\">这里</a> 更改本地设置。",
"wphl-welcome-upgrade": "<b>Wikiplus-highlight</b> 已升级到 $1。<br>您可能需要点击 <a id=\"wphl-settings-notify\" href=\"#\">这里</a> 查看并更新本地设置。",
"wphl-welcome-new-addon": "<b>Wikiplus-highlight</b> 已升级到 $1。<br>新版本添加了 $2 个插件,您可能需要点击 <a id=\"wphl-settings-notify\" href=\"#\">这里</a> 查看并更新本地设置。"
}

查看文件

@ -1,35 +0,0 @@
{
"wphl-version": "2.60.3",
"wphl-lang": "zh-hant",
"wphl-addon-search": "<span title=\"搜尋字串或正規表示式\">搜尋</span>",
"wphl-addon-activeline": "突出顯示游標所在行",
"wphl-addon-trailingspace": "顯示尾隨空格",
"wphl-addon-matchbrackets": "突出顯示匹配和未匹配的括號",
"wphl-addon-closebrackets": "自動閉合括號和引號 ",
"wphl-addon-matchtags": "突出顯示匹配和未匹配的標籤",
"wphl-addon-fold": "摺疊模板參數和標籤內部文字",
"wphl-addon-wikieditor": "添加WikiEditor工具條",
"wphl-addon-escape": "<span title=\"Ctrl/Cmd + / : HTML編碼\nCtrl/Cmd + \\ : URL解碼或編碼\">添加HTML和URL編碼快速鍵</span>",
"wphl-addon-contextmenu": "右鍵點擊可打開模板或模組頁面",
"wphl-addon-indentwithspace": "代碼縮排使用空格而非Tab",
"wphl-addon-indent": "縮排",
"wphl-addon-othereditors": "將選中的外掛程式用於其他編輯器",
"wphl-addon-lint": "維基語法檢查(使用 <code>Ctrl/Cmd + L</code> 開關)",
"wphl-addon-label": "請選擇您希望載入的外掛程式",
"wphl-addon-notice": "更改將於打開新的Wikiplus編輯區時生效。",
"wphl-addon-title": "Wikiplus突顯外掛程式",
"wphl-search-placeholder": "使用字串或正規表示式搜尋",
"wphl-replace-placeholder": "用於全文替換的字符串",
"wphl-search-replace": "全文替換",
"wphl-replace-count": "全文共有 $1 處待替換,確認替換嗎?",
"wphl-unfold": "點擊以展開$1",
"wphl-fold": "點擊以摺疊$1",
"wphl-fold-template": "模板參數",
"wphl-fold-comment": "HTML注釋",
"wphl-contentmodel": "請選擇內容模型:",
"wphl-feedback": "歡迎將錯誤報告或是改進建議提交到 <a href=\"https://github.com/bhsd-harry/Wikiplus-highlight/issues\" target=\"_blank\">GitHub</a> ",
"wphl-portlet": "Wikiplus突顯設定",
"wphl-welcome": "歡迎使用 <b>Wikiplus-highlight</b><br>您可以點擊 <a id=\"wphl-settings-notify\" href=\"#\">這裡</a> 更改本地設定。",
"wphl-welcome-upgrade": "<b>Wikiplus-highlight</b> 已升級到 $1。<br>您可能需要點擊 <a id=\"wphl-settings-notify\" href=\"#\">這裡</a> 檢視並更新本地設定。",
"wphl-welcome-new-addon": "<b>Wikiplus-highlight</b> 已升級到 $1。<br>新版本添加了 $2 個外掛程式,您可能需要點擊 <a id=\"wphl-settings-notify\" href=\"#\">這裡</a> 檢視並更新本地設定。"
}

200
lint.js
查看文件

@ -1,200 +0,0 @@
/**
* @author Bhsd <https://github.com/bhsd-harry>
* @license GPL-3.0
*/
(() => {
/* global wikiparse */
'use strict';
/** 加载 I18N */
const {libs: {wphl: {version, storage, addons, lintOptions, CDN, PARSER_CDN, isPc, getMwConfig}}} = mw,
/** @type {Record<string, string>} */ i18n = storage.getObject('wikiparser-i18n') || {},
/** @type {Record<string, string>} */ i18nLanguages = {
zh: 'zh-hans',
'zh-hans': 'zh-hans',
'zh-cn': 'zh-hans',
'zh-my': 'zh-hans',
'zh-sg': 'zh-hans',
'zh-hant': 'zh-hant',
'zh-tw': 'zh-hant',
'zh-hk': 'zh-hant',
'zh-mo': 'zh-hant',
},
i18nLang = i18nLanguages[mw.config.get('wgUserLanguage')],
I18N_CDN = i18nLang && `${CDN}/${PARSER_CDN}/i18n/${i18nLang}.json`;
if (i18nLang) {
(async () => {
if (i18n['wphl-version'] !== version || i18n['wphl-lang'] !== i18nLang) {
Object.assign(
i18n,
await $.ajax(`${I18N_CDN}`, {dataType: 'json', cache: true}),
{'wphl-version': version, 'wphl-lang': i18nLang},
);
storage.setObject('wikiparser-i18n', i18n);
}
wikiparse.setI18N(i18n);
})();
}
const include = mw.config.get('wgNamespaceNumber') === 10 && !mw.config.get('wgPageName').endsWith('/doc'),
{cmpPos, Pos} = CodeMirror;
/**
* annotationSource
* @param {string} str wikitext
* @param {CodeMirror.Editor} cm CodeMirror实例
* @returns {Promise<CodeMirror.LintAnnotation>}
*/
const annotate = async (str, _, cm) => {
const errors = await cm.Linter.queue(str);
return errors.map(error => {
const {startLine, startCol, endLine, endCol, message, severity} = error,
lineErrors = errors.filter(({startLine: line}) => line === startLine);
return {
message: message + '\u200B'.repeat(lineErrors.indexOf(error)),
severity,
from: Pos(startLine, startCol),
to: Pos(endLine, endCol),
};
});
};
CodeMirror.registerHelper('lint', 'mediawiki', annotate);
/**
* start linting
* @param {CodeMirror.Editor} cm CodeMirror实例
*/
const lint = async cm => {
const mode = cm.getOption('mode');
if (mode !== 'mediawiki' && mode !== 'text/mediawiki') {
return;
} else if (!wikiparse.config) {
const {config: {values: {wgFormattedNamespaces, wgNamespaceIds}}} = mw,
{parserFunction: [withPound,, ...modifiers]} = await wikiparse.getConfig(),
valuesWithPound = new Set(Object.values(withPound));
let mwConfig = cm.getOption('mwConfig');
if (!mwConfig.img || !mwConfig.variants || Object.values(mwConfig.functionSynonyms[0]).includes(true)) {
mwConfig = await getMwConfig('mediawiki');
}
const {tags, functionSynonyms: [insensitive, sensitive], doubleUnderscore, img, variants} = mwConfig;
for (const [k, v] of Object.entries(insensitive)) {
if (valuesWithPound.has(v) && !k.startsWith('#')) {
delete insensitive[k];
insensitive[`#${k}`] = v;
}
}
wikiparse.config = {
ext: Object.keys(tags),
namespaces: wgFormattedNamespaces,
nsid: wgNamespaceIds,
parserFunction: [
insensitive,
Object.keys(sensitive),
...modifiers,
],
doubleUnderscore: doubleUnderscore.map(Object.keys),
img: Object.fromEntries(Object.entries(img).map(([k, v]) => [k, v.slice(4)])),
variants,
};
wikiparse.setConfig(wikiparse.config);
}
cm.Linter = new wikiparse.Linter(include);
cm.setOption('scrollButtonHeight', 0);
let /** @type {CodeMirror.LintAnnotation[]} */ errors, /** @type {CodeMirror.LintAnnotation[]} */ warnings;
const /** @type {Map<CodeMirror.LintAnnotation[], number>} */ positionMap = new Map();
/**
* jump to next mark
* @param {CodeMirror.LintAnnotation[]} annotations 错误标记
*/
const nextMark = annotations => {
const {length} = annotations;
if (length > 0) {
const cursor = cm.getCursor(),
iNext = Math.max(0, annotations.findIndex(({from}) => cmpPos(from, cursor) >= 0)),
offset = positionMap.get(annotations) || 0;
cm.scrollIntoView(annotations[(iNext + offset) % length].from);
positionMap.set(annotations, offset + 1);
}
};
const annotateScrollWarn = cm.annotateScrollbar('CodeMirror-lint-scroll-warn'),
annotateScrollError = cm.annotateScrollbar('CodeMirror-lint-scroll-error'),
$panelErrorCount = $('<span>', {class: 'wphl-lint-count'}),
$panelWarnCount = $('<span>', {class: 'wphl-lint-count'}),
$panelError = $('<span>', {
class: 'wphl-lint-subpanel',
html: [$('<span>', {class: 'CodeMirror-lint-marker CodeMirror-lint-marker-error'}), $panelErrorCount],
}).click(() => {
nextMark(errors);
}),
$panelWarn = $('<span>', {
class: 'wphl-lint-subpanel',
html: [$('<span>', {class: 'CodeMirror-lint-marker CodeMirror-lint-marker-warning'}), $panelWarnCount],
}).click(() => {
nextMark(warnings);
}),
$panelElement = $('<div>', {id: 'wphl-lint-panel', html: [$panelError, $panelWarn]}),
$lineDiv = $(cm.display.lineDiv);
/**
* update linting
* @param {CodeMirror.LintAnnotation[]} annotations all annotations
*/
const onUpdateLinting = annotations => {
errors = annotations.filter(({severity}) => severity !== 'warning');
warnings = annotations.filter(({severity}) => severity === 'warning');
annotateScrollWarn.update(warnings);
annotateScrollError.update(errors);
$panelErrorCount.text(errors.length);
$panelWarnCount.text(warnings.length);
},
performLint = () => {
cm.performLint();
},
onInput = () => {
clearTimeout(cm.state.lint.timeout);
},
switchOption = () => {
if (cm.state.lint) {
cm.setOption('lint', false);
$lineDiv.off('input', onInput);
annotateScrollWarn.update([]);
annotateScrollError.update([]);
$panelElement.detach();
} else {
cm.setOption('lint', {
delay: 1000, ...lintOptions, selfContain: true, onUpdateLinting,
});
$lineDiv.on('input', onInput);
$panelElement.insertAfter(cm.getWrapperElement());
}
};
cm.setOption('gutters', ['CodeMirror-lint-markers']);
switchOption();
const ctrl = isPc(CodeMirror) ? 'Ctrl' : 'Cmd';
cm.addKeyMap({[`${ctrl}-K`]: performLint, [`${ctrl}-L`]: switchOption});
cm.on('cursorActivity', () => {
positionMap.clear();
});
};
/**
* 分离hook函数以便调试
* @param {CodeMirror.Editor} cm CodeMirror实例
*/
const hook = cm => {
if (addons.has('lint') && cm.getTextArea
&& (addons.has('otherEditors') || cm.getTextArea().id === 'Wikiplus-Quickedit')
) {
lint(cm);
}
};
mw.hook('wiki-codemirror').add(hook);
mw.hook('InPageEdit.quickEdit.codemirror').add(
/** @param {{cm: CodeMirror.Editor}} */ ({cm: doc}) => hook(doc),
);
mw.hook('inspector').add(hook);
mw.libs.wphl.lintHook = hook;
})();

1006
main.js

文件差异内容过多而无法显示 加载差异

查看文件

@ -1,324 +0,0 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
// Modified for MediaWiki by Bhsd <https://github.com/bhsd-harry>
(() => {
'use strict';
const {Pos, cmpPos, Init} = CodeMirror,
tagStart = /<(\/?)([a-z]\w*)(?=[\s/>]|$)/giu,
voidTags = new Set(['br', 'wbr', 'hr', 'img']),
maxScanLines = 1000;
/** @ignore */
class Iter {
/**
* @param {CodeMirror.Editor} cm CodeMirror实例
* @param {CodeMirror.Position} pos 当前位置
*/
constructor(cm, {line, ch}) {
this.line = line;
this.ch = ch;
this.cm = cm;
this.text = cm.getLine(line);
this.min = Math.max(line - maxScanLines + 1, cm.firstLine());
this.max = Math.min(line + maxScanLines - 1, cm.lastLine());
}
/** 提取当前位置 */
get pos() {
return Pos(this.line, this.ch);
}
/** 是否是标签 */
isTag() {
const type = this.cm.getTokenTypeAt(this.pos);
return /\b(?:mw-(?:html|ext)tag|tag\b)/u.test(type);
}
/**
* 判断是否是`<``>`
* @param {number} ch 列号
*/
bracketAt(ch) {
const type = this.cm.getTokenTypeAt(Pos(this.line, ch + 1));
return /\b(?:mw-(?:html|ext)tag-)?bracket\b/u.test(type);
}
/** Jump to the start of the next line */
nextLine() {
if (this.line >= this.max) {
return false;
}
this.ch = 0;
this.text = this.cm.getLine(++this.line);
return true;
}
/** Jump to the end of the previous line */
prevLine() {
if (this.line <= this.min) {
return false;
}
this.text = this.cm.getLine(--this.line);
this.ch = this.text.length;
return true;
}
/** Jump to the letter after a `>` towards the line end */
toTagEnd() {
for (;;) {
const gt = this.text.indexOf('>', this.ch);
if (gt === -1) {
if (this.nextLine()) {
continue;
}
return undefined;
}
this.ch = gt + 1;
if (this.bracketAt(gt)) {
return this.text[gt - 1] === '/' ? 'selfClose' : 'regular';
}
}
}
/** Jump to a `<` towards the line start */
toTagStart() {
for (;;) {
const lt = this.ch > 0 ? this.text.lastIndexOf('<', this.ch - 1) : -1;
if (lt === -1) {
if (this.prevLine()) {
continue;
}
return undefined;
} else if (!this.bracketAt(lt)) {
this.ch = lt;
continue;
}
tagStart.lastIndex = lt;
this.ch = lt;
const match = tagStart.exec(this.text);
if (match && match.index === lt) {
return match;
}
}
}
/** Jump to the start of the last line, or the letter after a `tagStart` */
toNextTag() {
for (;;) {
tagStart.lastIndex = this.ch;
const found = tagStart.exec(this.text);
if (!found) {
if (this.nextLine()) {
continue;
}
return undefined;
}
this.ch = found.index + found[0].length;
if (this.bracketAt(found.index)) {
return found;
}
}
}
/** Jump to the end of the first line, or a non-bracket `>`, or the letter after a tag bracket `>` */
toPrevTag() {
for (;;) {
const gt = this.ch > 0 ? this.text.lastIndexOf('>', this.ch - 1) : -1;
if (gt === -1) {
if (this.prevLine()) {
continue;
}
return undefined;
} else if (this.bracketAt(gt)) {
this.ch = gt + 1;
return this.text[gt - 1] === '/' ? 'selfClose' : 'regular';
}
this.ch = gt;
}
}
// eslint-disable-next-line jsdoc/require-returns-check
/**
* 搜索匹配的闭合标签
* @param {string} tag 标签名
* @returns {CodeMirror.MatchingTag}
*/
findMatchingClose(tag) {
const /** @type {string[]} */ stack = [];
for (;;) {
const next = this.toNextTag();
if (!next) {
return undefined;
}
const from = Pos(this.line, this.ch - next[0].length),
end = this.toTagEnd(),
tagName = next[2].toLowerCase();
if (!end) {
return undefined;
} else if (end === 'selfClose' || voidTags.has(tagName)) {
continue;
} else if (next[1]) { // closing tag
let i = stack.length - 1;
for (; i >= 0; --i) {
if (stack[i] === tagName) {
stack.length = i;
break;
}
}
if (i < 0 && (!tag || tag === tagName)) {
return {tag: tagName, from, to: this.pos};
}
} else { // opening tag
stack.push(tagName);
}
}
}
// eslint-disable-next-line jsdoc/require-returns-check
/**
* 搜索匹配的开启标签
* @param {string|undefined} tag 标签名
* @returns {CodeMirror.MatchingTag}
*/
findMatchingOpen(tag) {
const /** @type {string[]} */ stack = [];
for (;;) {
const prev = this.toPrevTag();
if (!prev) {
return undefined;
}
const {pos: to} = this,
start = this.toTagStart();
if (!start) {
return undefined;
}
const tagName = start[2].toLowerCase();
if (prev === 'selfClose' || voidTags.has(tagName)) {
continue;
} else if (start[1]) { // closing tag
stack.push(tagName);
} else { // opening tag
let i = stack.length - 1;
for (; i >= 0; --i) {
if (stack[i] === tagName) {
stack.length = i;
break;
}
}
if (i < 0 && (!tag || tag === tagName)) {
return {tag: tagName, from: this.pos, to};
}
}
}
}
}
CodeMirror.defineExtension(
'findMatchingTag',
/**
* @this {CodeMirror.Editor}
* @param {CodeMirror.Position} pos 当前位置
* @returns {CodeMirror.MatchingTagPair}
*/
function(pos) {
const iter = new Iter(this, pos);
if (!iter.isTag()) {
return undefined;
}
const end = iter.toTagEnd(),
to = end && iter.pos,
start = end && iter.toTagStart();
if (!start || cmpPos(iter, pos) > 0) {
return undefined;
}
const tag = start[2].toLowerCase(),
here = {from: iter.pos, to, tag};
if (end === 'selfClose' || voidTags.has(tag)) {
return {open: here, loc: 'self'};
} else if (start[1]) { // closing tag
return {open: iter.findMatchingOpen(tag), close: here, loc: 'close'};
}
// opening tag
return {open: here, close: new Iter(this, to).findMatchingClose(tag), loc: 'open'};
},
);
CodeMirror.defineExtension(
'findEnclosingTag',
/**
* @this {CodeMirror.Editor}
* @param {CodeMirror.Position} pos
* @param {string} tag
* @returns {CodeMirror.MatchingTagPair}
*/
function(pos, tag) {
const open = new Iter(this, pos).findMatchingOpen(tag);
if (open) {
const close = new Iter(this, pos).findMatchingClose(open.tag);
return close && {open, close};
}
return undefined;
},
);
/** Used by addon/edit/closetag.js */
CodeMirror.scanForClosingTag = (cm, pos, tagName) => new Iter(cm, pos).findMatchingClose(tagName);
CodeMirror.defineOption('matchTags', false, (cm, val, old) => {
if (old && old !== Init) {
cm.off('cursorActivity', doMatchTags);
clear(cm);
}
if (val) {
cm.on('cursorActivity', doMatchTags);
doMatchTags(cm);
}
});
/**
* 清除高亮
* @param {CodeMirror.EditorWithMatchingTags} cm CodeMirror实例
*/
const clear = cm => {
if (cm.state.tagHit) {
cm.state.tagHit.clear();
cm.state.tagHit = undefined;
}
if (cm.state.tagOther) {
cm.state.tagOther.clear();
cm.state.tagOther = undefined;
}
};
/**
* 搜索并高亮匹配的标签
* @param {CodeMirror.EditorWithMatchingTags} cm CodeMirror实例
*/
const doMatchTags = cm => {
cm.operation(() => {
clear(cm);
if (cm.somethingSelected()) {
return;
}
const match = cm.findMatchingTag(cm.getCursor());
if (!match) {
return;
} else if (match.loc === 'self') {
cm.state.tagHit = cm.markText(match.open.from, match.open.to, {className: 'cm-matchingtag'});
return;
}
const hit = match.loc === 'open' ? match.open : match.close,
other = match.loc === 'close' ? match.open : match.close;
if (hit) {
cm.state.tagHit = cm.markText(hit.from, hit.to, {className: `cm-${other ? '' : 'non'}matchingtag`});
if (other) {
cm.state.tagOther = cm.markText(other.from, other.to, {className: 'cm-matchingtag'});
}
}
});
};
})();

查看文件

@ -19,21 +19,25 @@
"/*.js",
"/*.css"
],
"browser": "/main.js",
"browser": "/dist/main.js",
"repository": {
"type": "git",
"url": "git+https://github.com/bhsd-harry/Wikiplus-highlight.git"
},
"scripts": {
"test": "eslint . && stylelint styles.css && http-server -c-1 --cors"
"lint:ts": "tsc --noEmit && eslint --cache .",
"lint:css": "stylelint *.css",
"lint": "npm run lint:ts && npm run lint:css",
"build": "tsc"
},
"devDependencies": {
"@bhsd/codemirror-mediawiki": "^2.4.2",
"@stylistic/eslint-plugin": "^1.5.4",
"@stylistic/stylelint-plugin": "^2.0.0",
"@types/jquery": "^3.5.29",
"@types/oojs-ui": "^0.47.6",
"@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.19.1",
"esbuild": "^0.19.12",
"eslint": "^8.56.0",
"eslint-plugin-es-x": "^7.5.0",
"eslint-plugin-eslint-comments": "^3.2.0",
@ -42,7 +46,6 @@
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-regexp": "^2.2.0",
"eslint-plugin-unicorn": "^50.0.1",
"http-server": "^14.1.0",
"stylelint": "^16.1.0",
"stylelint-config-recommended": "^14.0.0",
"types-mediawiki": "^1.4.0",

258
search.js
查看文件

@ -1,258 +0,0 @@
/**
* @author Bhsd <https://github.com/bhsd-harry>
* @license GPL-3.0
*/
/* eslint-disable require-unicode-regexp */
(() => {
'use strict';
const {Pos} = CodeMirror,
{libs: {wphl: {msg, isPc, addons}}} = mw;
// Prepare elements
const $search = $('<input>', {class: 'Wikiplus-Quickedit-Search', placeholder: msg('search-placeholder')}),
$searchClose = $('<span>', {text: '×', class: 'Wikiplus-Symbol-Btn'}),
$searchNext = $('<span>', {text: '▼', class: 'Wikiplus-Symbol-Btn'}),
$searchPrev = $('<span>', {text: '▲', class: 'Wikiplus-Symbol-Btn'}),
$searchContainer = $('<div>', {
class: 'Wikiplus-Quickedit-Search-Div',
html: [$search, $searchNext, $searchPrev, $searchClose],
}),
$searchBtn = $('<span>', {class: 'Wikiplus-Btn', html: msg('addon-search')}),
$replace = $('<textarea>', {class: 'Wikiplus-Quickedit-Search', placeholder: msg('replace-placeholder'), rows: 1}),
$replaceClose = $('<span>', {text: '×', class: 'Wikiplus-Symbol-Btn'}),
$replaceNext = $('<span>', {text: '▼', class: 'Wikiplus-Symbol-Btn'}),
$replacePrev = $('<span>', {text: '▲', class: 'Wikiplus-Symbol-Btn'}),
$replaceContainer = $('<div>', {
class: 'Wikiplus-Quickedit-Search-Div',
html: [$replace, $replaceNext, $replacePrev, $replaceClose],
}),
$replaceBtn = $('<span>', {class: 'Wikiplus-Btn', html: msg('search-replace')});
const escapeRegExp = mw.util.escapeRegExp || mw.RegExp.escape;
const /** @type {CodeMirror.Mode<undefined>} */ overlay = {token: /** @override */ () => {}};
/**
* 根据搜索字符串生成高亮
* @param {string|RegExp} str 搜索字符串
*/
const token = str => {
const initial = typeof str === 'string' && new RegExp(`[^${escapeRegExp(str[0])}]`, 'i');
/**
* @override
* @param {CodeMirror.StringStream} stream
*/
return stream => {
if (stream.match(str, true, true)) {
return 'search';
}
stream.next();
if (initial) {
stream.eatWhile(initial);
}
return undefined;
};
};
/** input event handler of `$search` */
const onInput = () => {
$search.css('background-color', '').off('input', onInput);
};
let /** @type {string} */ lastSource,
/** @type {string|RegExp} */ lastPtn,
/** @type {CodeMirror.SearchCursor} */ cursor;
/**
* 更新搜索字符串
* @param {CodeMirror.Editor} cm CodeMirror实例
*/
const updatePtn = cm => {
const /** @type {string} */ source = $search.val();
if (!source) {
return undefined;
} else if (source === lastSource) {
return lastPtn;
}
const caseFold = source.endsWith('i'),
ptn = /^\/.+\/i?$/u.test(source)
? new RegExp(source.slice(1, caseFold ? -2 : -1), caseFold ? 'im' : 'm')
: source;
cm.removeOverlay(overlay);
overlay.token = token(ptn);
cm.addOverlay(overlay);
lastSource = source;
lastPtn = ptn;
cursor = cm.getSearchCursor(ptn, cm.getCursor(), {caseFold: true});
return ptn;
};
/**
* keyboard event handler of `$search`
* @param {CodeMirror.Editor} cm CodeMirror实例
* @param {boolean} dir 搜索方向
* @param {boolean} update 是否先更新搜索字符串
*/
const findNext = (cm, dir, update = true) => {
if (update && !updatePtn(cm)) {
return;
}
let result = dir ? cursor.findNext() : cursor.findPrevious();
if (!result) {
let pos;
if (dir) {
pos = Pos(0, 0);
} else {
const lastLine = cm.lastLine();
pos = Pos(lastLine, cm.getLine(lastLine).length);
}
cursor.pos = {from: pos, to: pos};
cursor.atOccurrence = false;
result = dir ? cursor.findNext() : cursor.findPrevious();
}
if (result) {
const from = cursor.from(),
to = cursor.to();
cm.setSelection(from, to);
cm.scrollIntoView({from, to});
onInput();
} else {
$search.css('background-color', 'pink').on('input', onInput);
}
};
/**
* replace one by one
* @param {CodeMirror.Editor} cm CodeMirror实例
* @param {boolean} dir 搜索方向
*/
const replaceNext = (cm, dir) => {
const ptn = updatePtn(cm);
if (!ptn) {
return;
} else if (cursor.atOccurrence) {
const replace = $replace.val();
cursor.replace(typeof ptn === 'string' ? replace : cursor.pos.match[0].replace(ptn, replace));
}
findNext(cm, dir, false);
};
/**
* keyboard event handler of `$replace`
* @param {CodeMirror.Editor} cm CodeMirror实例
*/
const replace = async cm => {
const ptn = updatePtn(cm);
if (!ptn) {
return;
}
const replacePtn = typeof ptn === 'string'
? new RegExp(escapeRegExp(ptn), 'gim')
: new RegExp(ptn, `g${ptn.flags}`),
val = cm.getValue(),
mt = val.match(replacePtn);
if (mt && await OO.ui.confirm(msg('replace-count', mt.length))) {
const {left, top} = cm.getScrollInfo();
cm.setValue(val.replace(replacePtn, $replace.val()));
cm.scrollTo(left, top);
$replaceContainer.hide();
}
};
/** click event handler of `$searchBtn` */
const findNew = () => {
$searchContainer.show();
$search.select().focus()[0]
.scrollIntoView({behavior: 'smooth'});
};
/** click event handler of `$replaceBtn` */
const replaceNew = () => {
$replaceContainer.show();
if ($searchContainer.is(':hidden')) {
findNew();
}
};
/**
* click event handler of `$searchClose`
* @param {CodeMirror.Editor} cm CodeMirror实例
*/
const reset = cm => {
cm.removeOverlay(overlay);
$searchContainer.hide();
$replaceContainer.hide();
lastSource = '';
};
CodeMirror.commands.find = findNew;
CodeMirror.commands.findNext = /** 向后搜索 */ doc => {
findNext(doc, true);
};
CodeMirror.commands.findPrev = /** 向前搜索 */ doc => {
findNext(doc, false);
};
CodeMirror.commands.replace = replaceNew;
CodeMirror.commands.replaceNext = /** 向后替换 */ doc => {
replaceNext(doc, true);
};
CodeMirror.commands.replaceAll = /** 全文替换 */ doc => { // eslint-disable-line es-x/no-string-prototype-replaceall
replace(doc);
};
mw.hook('wiki-codemirror').add(/** @param {CodeMirror.Editor} cm */ cm => {
if (!cm.getOption('styleSelectedText') || addons.has('wikiEditor')) {
return;
}
const $textarea = $(cm.getWrapperElement()).prev('#Wikiplus-Quickedit');
if ($textarea.length === 0) {
return;
}
$searchContainer.hide().insertBefore($textarea);
$searchBtn.click(findNew).insertAfter('#Wikiplus-Quickedit-Jump');
$searchClose.click(() => {
reset(cm);
});
$searchNext.click(() => {
findNext(cm, true);
});
$searchPrev.click(() => {
findNext(cm, false);
});
$search.val('').keydown(e => {
if (e.key === 'Enter') {
e.preventDefault();
findNext(cm, true);
} else if (e.key === 'Escape') {
e.stopPropagation();
reset(cm);
}
});
$replaceContainer.hide().insertBefore($textarea);
$replaceBtn.click(replaceNew).insertAfter($searchBtn);
$replaceClose.click(() => {
$replaceContainer.hide();
});
$replaceNext.click(() => {
replaceNext(cm, true);
});
$replacePrev.click(() => {
replaceNext(cm, false);
});
$replace.val('').keydown(e => {
if (e.key === 'Enter') {
e.preventDefault();
replace(cm);
} else if (e.key === 'Escape') {
e.stopPropagation();
$replaceContainer.hide();
}
});
const ctrl = isPc(CodeMirror) ? 'Ctrl' : 'Cmd';
cm.addKeyMap({
[`${ctrl}-H`]: 'replace',
[`Shift-${ctrl}-H`]: 'replaceNext',
[`${ctrl}-Alt-Enter`]: 'replaceAll',
});
});
})();

14
src/index.d.ts vendored 普通文件
查看文件

@ -0,0 +1,14 @@
import 'types-mediawiki';
import type {CodeMirror6} from '@bhsd/codemirror-mediawiki';
declare global {
namespace mw {
const libs: Record<string, unknown>;
}
const CodeMirror6: {
fromTextArea(textarea: HTMLTextAreaElement, lang?: string, ns?: number): Promise<CodeMirror6>;
};
}
export {};

109
src/main.ts 普通文件
查看文件

@ -0,0 +1,109 @@
/**
* @name Wikiplus-highlight Wikiplus编辑器的CodeMirror语法高亮扩展
* @author Bhsd <https://github.com/bhsd-harry>
* @license GPL-3.0
*/
((): void => {
if ('wphl' in mw.libs) {
return;
}
const version = '3.0.0';
mw.libs['wphl'] = {version}; // 开始加载
// 路径
const CDN = '//testingcf.jsdelivr.net',
MW_CDN = 'npm/@bhsd/codemirror-mediawiki@2.4.2/dist/mw.min.js',
REPO_CDN = 'npm/wikiplus-highlight';
const {
wgPageName: page,
wgNamespaceNumber: ns,
wgPageContentModel: contentmodel,
} = mw.config.get();
const CONTENTMODEL: Record<string, string> = {
'sanitized-css': 'css',
wikitext: 'mediawiki',
};
/** 根据需要加载CodeMirror6 */
const init = (): Promise<void> =>
'CodeMirror6' in window
? Promise.resolve()
: new Promise(resolve => {
const script = document.createElement('script');
script.addEventListener('load', () => {
resolve();
});
script.type = 'module';
script.src = `${CDN}/${MW_CDN}`;
document.head.appendChild(script);
});
/** 检查页面语言类型 */
const getPageMode = async (): Promise<string> => {
if (ns !== 274 && contentmodel !== 'Scribunto' || page.endsWith('/doc')) {
return CONTENTMODEL[contentmodel] || contentmodel;
}
await mw.loader.using('oojs-ui-windows');
if (
await OO.ui.confirm(mw.msg('cm-mw-contentmodel'), {
actions: [{label: ns === 274 ? 'Widget' : 'Lua'}, {label: 'Wikitext', action: 'accept'}],
})
) {
return 'mediawiki';
}
return ns === 274 ? 'html' : 'lua';
};
/**
*
* @param $target
* @param setting Wikiplus设置使json语法
*/
const renderEditor = async ($target: JQuery<HTMLTextAreaElement>, setting: boolean): Promise<void> => {
await init();
const cm = await CodeMirror6.fromTextArea($target[0]!, setting ? 'json' : await getPageMode());
cm.view.dom.id = 'Wikiplus-CodeMirror';
document.querySelector<HTMLAnchorElement>('#Wikiplus-Quickedit-Jump > a')!.href = '#Wikiplus-CodeMirror';
if (!setting) { // 普通Wikiplus编辑区
const settings: Record<string, unknown> | null
= JSON.parse(String(localStorage.getItem('Wikiplus_Settings'))),
escToExitQuickEdit = settings && (settings['esc_to_exit_quickedit'] || settings['escToExitQuickEdit']),
submit = /** 提交编辑 */ (): true => {
document.getElementById('Wikiplus-Quickedit-Submit')!.dispatchEvent(new MouseEvent('click'));
return true;
},
submitMinor = /** 提交小编辑 */ (): true => {
document.querySelector<HTMLInputElement>('#Wikiplus-Quickedit-MinorEdit')!.checked = true;
return submit();
},
escapeEdit = /** 按下Esc键退出编辑 */ (): true => {
document.getElementById('Wikiplus-Quickedit-Back')!.dispatchEvent(new MouseEvent('click'));
return true;
};
cm.extraKeys([
{key: 'Mod-S', run: submit},
{key: 'Shift-Mod-S', run: submitMinor},
...escToExitQuickEdit === true || escToExitQuickEdit === 'true'
? [{key: 'Esc', run: escapeEdit}]
: [],
]);
}
};
// 监视 Wikiplus 编辑框
const observer = new MutationObserver(records => {
const $editArea = $(records.flatMap(({addedNodes}) => [...addedNodes]))
.find<HTMLTextAreaElement>('#Wikiplus-Quickedit, #Wikiplus-Setting-Input');
if ($editArea.length > 0) {
void renderEditor($editArea, $editArea.attr('id') === 'Wikiplus-Setting-Input');
}
});
observer.observe(document.body, {childList: true});
mw.loader.load(`${CDN}/${REPO_CDN}@${version}/styles.min.css`, 'text/css');
})();

查看文件

@ -1,145 +1,7 @@
#Wikiplus-CodeMirror {
border: 1px solid #c8ccd1;
line-height: 1.3;
clear: both;
/* fix mobile select */
-webkit-user-select: auto;
user-select: auto;
}
#Wikiplus-CodeMirror .CodeMirror-gutter-wrapper {
/* fix iOS select-all */
-webkit-user-select: none;
user-select: none;
}
div.Wikiplus-InterBox {
font-size: 14px;
z-index: 100;
}
.skin-minerva .Wikiplus-InterBox {
font-size: 16px;
}
.cm-trailingspace {
text-decoration: underline wavy #f00;
}
div.CodeMirror span.CodeMirror-matchingbracket {
box-shadow: 0 0 0 2px #9aef98;
}
div.CodeMirror span.CodeMirror-nonmatchingbracket {
box-shadow: 0 0 0 2px #eace64;
}
#Wikiplus-highlight-dialog .oo-ui-messageDialog-title {
margin-bottom: .28571429em; /* stylelint-disable-line number-max-precision */
}
#Wikiplus-highlight-dialog .oo-ui-flaggedElement-notice {
font-weight: normal;
margin: 0;
}
.CodeMirror-contextmenu .cm-mw-template-name {
cursor: pointer;
}
.skin-moeskin #ca-more-actions li > a {
display: inline-block;
padding: .4rem .8rem;
line-height: 1.5;
}
.skin-moeskin .oo-ui-windowManager-modal > .oo-ui-dialog {
z-index: 101;
}
/* search.js */
.Wikiplus-Btn {
line-height: 1.4;
}
.Wikiplus-Quickedit-Search-Div {
margin: 7px 0 5px;
}
.Wikiplus-Symbol-Btn {
font-size: 20px;
margin: 7px;
vertical-align: middle;
cursor: pointer;
}
.Wikiplus-Quickedit-Search {
width: 50%;
padding: revert;
border: revert;
background: revert;
vertical-align: middle;
font: inherit;
box-sizing: border-box;
}
textarea.Wikiplus-Quickedit-Search {
display: inline;
resize: none;
}
.cm-search {
background-color: rgba(255, 192, 203, .51);
}
/* matchtags.js */
.cm-matchingtag {
background-color: #c9ffc8;
}
.cm-nonmatchingtag {
background-color: #fff0a8;
}
/* fold.js */
.CodeMirror-sizer {
overflow: visible;
}
.CodeMirror-tooltip {
position: absolute;
z-index: 101;
cursor: pointer;
background-color: #ffd;
border: 1px solid;
padding: 0 1px;
font-size: 10pt;
line-height: 1.2;
}
.CodeMirror-widget-unfold {
cursor: pointer;
border: 1px dotted;
}
/* lint.js */
.CodeMirror-line span.CodeMirror-lint-mark-warning {
background: #ffbf00;
color: #fff;
caret-color: #000;
}
.CodeMirror-line span.CodeMirror-lint-mark-error {
background: #d33;
color: #fff;
}
.CodeMirror-lint-scroll-warn {
background: #fc3;
border-top: 1px solid #fc3;
border-bottom: 1px solid #fc3;
box-sizing: border-box;
}
.CodeMirror-lint-scroll-error {
background: #d33;
border-top: 1px solid #d33;
border-bottom: 1px solid #d33;
box-sizing: border-box;
}
#wphl-lint-panel {
background: #f7f7f7;
border: 1px solid #c8ccd1;
}
.wphl-lint-subpanel {
margin-left: 1ch;
cursor: pointer;
}
.wphl-lint-count {
display: inline-block;
width: 5ch;
padding: 1px 0 1px 1ch;
}
/* markSelection.js */
.CodeMirror-line span.CodeMirror-selectedtext {
background: #d7d4f0;
}

查看文件

@ -1,14 +1,23 @@
{
"include": [
"src/*.ts"
],
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"target": "es2018",
"module": "CommonJS"
},
"typeAcquisition": {
"include": [
"jquery",
"codemirror"
]
"target": "ES2018",
"module": "CommonJS",
"declaration": false,
"outDir": "dist",
"alwaysStrict": true,
"exactOptionalPropertyTypes": true,
"noImplicitAny": true,
"noImplicitOverride": true,
"noImplicitThis": true,
"noPropertyAccessFromIndexSignature": true,
"noUncheckedIndexedAccess": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"useUnknownInCatchVariables": true,
"skipLibCheck": true
}
}