This commit is contained in:
2024-02-07 14:46:20 +04:00
parent 820a683319
commit dedc65ea7c
11 changed files with 555 additions and 2 deletions
+2
View File
@@ -1,3 +1,5 @@
.idea
# ---> Node
# Logs
logs
+14
View File
@@ -0,0 +1,14 @@
## Версия 0.2.0 ()
* Перевод кода на TypeScript.
*
## Версия 0.1.2 (22 апреля 2022 г.)
* Добавлены поле `editor` и методы `addEditorButton` и `generateEditorButtons`.
## Версия 0.1.1
* Добавлен метод `addAuthorEditor`.
## Версия 0.1.0
* Начальная версия.
* Обработка событий `pun_main_ready`, `pun_preview`, `pun_post`, `pun_edit`, `spoiler.firstOpen`.
* Методы `addAdapter`, `addTag`.
+14 -1
View File
@@ -1,3 +1,16 @@
# mybbcode
# MyBBCode
Гибкий инструмент для работы с BB-кодами на форумах, основанных на платформе mybb.ru.
# F.A.Q.
## Зачем нужна эта утилита?
Недавние изменения на MyBB, в частности, добавление пользовательских тегов и ajax в форме ответа, несколько изменили методы работу с пользовательскими тегами: первое изменение упростило, второе же усложнило. Это, впрочем, нормально, но, по меньшей мере, у меня случилась некоторая фрустрация оттого, как в некоторых местах приходиться работать с некоторыми тегами. Фрустрацию я преодолел, написав прослойку, которая позволяет при помощи относительно короткого и простого кода добавить методы для модификации.
## Что утилита делает?
Если простыми словами, то данный скрипт не делает ничего сам по себе, но он помогает облегчить жизнь тем, кто использует различные скрипты для модификации сообщений на форумах, обеспечивая простой доступ к
* обработке сообщений, в том числе загруженных через ajax, добавленных в предпросмотр и других местах,
* добавлению кнопок в форме ответа, а так же изменению порядка их отображения.
И всё это с использованием последних новинок на сервисе MyBB, которые могут позволить минимизировать количество кода для реализации простых функций.
+56
View File
@@ -0,0 +1,56 @@
{
"name": "mybbcode",
"version": "0.1.3",
"description": "Гибкий инструмент для работы с BB-кодами на форумах, основанных на платформе mybb.ru",
"main": "index.js",
"scripts": {
"build": "webpack --mode=production --node-env=production",
"build:dev": "webpack --mode=development",
"build:prod": "webpack --mode=production --node-env=production",
"watch": "webpack --watch"
},
"repository": {
"type": "git",
"url": "git+https://github.com/kozhilya/mybbcode.git"
},
"keywords": [
"mybb",
"bb",
"codes"
],
"author": "kozhilya",
"license": "MIT",
"bugs": {
"url": "https://github.com/kozhilya/mybbcode/issues"
},
"homepage": "https://github.com/kozhilya/mybbcode#readme",
"devDependencies": {
"@babel/cli": "^7.21.0",
"@babel/core": "^7.21.3",
"@babel/preset-env": "^7.20.2",
"@babel/preset-typescript": "^7.21.0",
"@types/jquery": "^3.5.29",
"@webpack-cli/generators": "^3.0.1",
"babelify": "^10.0.0",
"grunt": "^1.6.1",
"grunt-browserify": "^6.0.0",
"grunt-contrib-uglify": "^5.2.2",
"grunt-jsdoc-to-markdown": "^6.0.0",
"html-loader": "^4.2.0",
"jsdoc-babel": "^0.5.0",
"jsdoc-to-markdown": "^8.0.0",
"markdown-it": "^13.0.1",
"markdown-it-container": "^3.0.0",
"prettier": "^2.8.2",
"terser-webpack-plugin": "^5.3.7",
"ts-loader": "^9.4.2",
"tsify": "^5.0.4",
"typescript": "^5.3.3",
"uglify-js": "^3.17.4",
"webpack": "^5.76.3",
"webpack-cli": "^5.0.1"
},
"dependencies": {
"webpack-utf8-bom": "^1.4.0"
}
}
+31
View File
@@ -0,0 +1,31 @@
export class Editor {
/**
* Порядок отображения кнопок формы ответа.
*/
buttons: string[] = null;
/**
* Массив из 2 значений, которые являются HTML-кодами, что будут добавлены до и после списка кнопок.
* @type {string[]}
*/
wrapper: string[] = ['<table><tr>', '</tr></table>'];
/**
* Метод для генерации кнопок.
* @param {string} key Название кода, ключ объекта `FORUM.editor`.
* @param value Набор правил кода, соответствующее ключу key значения объекта `FORUM.editor[key]`.
*/
generator(key: string, value: any): string {
return `<td class="form-buttons-button" id="button-${key}" title="${value.name}"><img onclick="FORUM.get(&quot;editor.${key}.onclick()&quot;)" src="/i/blank.gif" alt="&nbsp;" /></td>`;
}
/**
* Дополнительные элементы, которые могут быть добавлены наряду с другими формы ответа
* @type {Object.<string, string>}
*/
specials: {[key: string] : string} = {
'|': '<td class="form-buttons-splitter">&nbsp;</td>',
'~': '<td class="form-buttons-filler">&nbsp;</td>',
'\n': '</tr><tr>',
};
}
+5
View File
@@ -0,0 +1,5 @@
import MybbCode from "./mybb-code";
(window as any).MyBBCode = (window as any).MyBBCode || (() => {
return new MybbCode();
})();
+260
View File
@@ -0,0 +1,260 @@
import {AdapterCallback, AuthorEditorCallback, EventResolver, TagCallback} from "./types";
import {FORUM, MybbEditorTag} from "./mybb-editor";
import {Editor} from "./editor";
export default class MybbCode {
/**
* Текст, который будет установлен при вызове `set_html(false)` в обработчиках тегов,
* добавленных при помощи метода `addTag`.
* @type loadingText {string}
* @see addTag
*/
loadingText = '\u0421\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0435 \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u0442\u0441\u044F...';
/**
* Набор тегов
*/
private tags: {[key: string]: TagCallback} = {};
/**
* Набор адаптеров
*/
private adapters: AdapterCallback[] = [];
/**
* Набор обработчиков столбца автора
*/
private authorEditors: AuthorEditorCallback[] = [];
/**
* Выделение контейнеров сообщений для событий
*/
events: {[key: string]: EventResolver} = {
'pun_preview': (_) => $('#post-preview .post-content'),
'pun_post': (_) => $('.topic .new-ajax .post-content'),
'pun_edit': (e) => $(e.sender),
'pun_main_ready': (_) => $('.post-content'),
'spoiler.firstOpen': (e) => $(e.sender).parent().find('blockquote').eq(0)
};
/**
* Набор правил для генерации кнопок формы ответа. Удаление полей настоятельно не рекомендуется.
*/
editor: Editor = new Editor();
/**
* Конструктор класса `MyBBCode`.
*/
constructor() {
$(document).on(Object.keys(this.events).join(' '), (event) => {
const blocks = this.events[event.type](event);
this.processPostContents(blocks);
});
if (FORUM.editor) {
this.editor.buttons = Object.keys(FORUM.editor).filter((v) => v !== 'settings');
$(document).on('pun_main_ready', () => this.generateEditorButtons());
}
}
/**
* Добавление нового события, который будет запускать обработчики сообщений и обработчики пользовательских кодов.
* @param {string} name Отслеживаемое DOM-событие, используемое методом `jQuery.on(name)`, и которое можно
* вызвать методом `jQuery.trigger(name)`.
* @param {EventResolver} resolver Это должна быть функция, которые на основе объекта события (`event`, он
* подаётся на вход этой функции), определяет, какие контейнеры должны быть
* обработаны обработчиками.
*/
registerEvent(name: string, resolver: EventResolver): void {
$(document).on(name, (event) => this.processPostContents(resolver(event)));
}
/**
* Установка HTML загружающегося тега.
* @param {JQuery} elem Цель установки
* @param {string|false} html HTML-код для установки
*/
private setHtml(elem: JQuery, html: string|false): void {
if (html === false) {
elem.html(this.loadingText).addClass('loading');
}
else {
elem.html(html).removeClass('loading');
}
}
/**
* Обработка контейнера сообщения всеми зарегистрированными обработчиками.
* @param containers jQuery-объект, ссылающийся на контейнер (или контейнеры), который содержит необработанное сообщение.
*/
private processPostContents(containers: JQuery): void {
containers.each((_, container) => {
const $container = $(container);
$.each(this.tags, (tag, action) => {
$('.custom_tag_' + tag, $container).each((i, elem) => {
const $elem = $(elem);
let id = $elem.prop('id') || (tag + '-' + i);
if ($elem.hasClass('processed')) {
return;
}
action($elem, '.custom_tag_' + tag + '#' + id, this.setHtml.bind(this, $elem));
$elem.addClass('processed');
});
});
$.each(this.adapters, (i, action) => {
action($container);
});
if ($container.hasClass('post-content')) {
const ul = $container.parents('.post').find('.post-author ul');
if (!ul.hasClass('mybbcode-processed')) {
$.each(this.authorEditors, (i, action) => {
action(ul);
});
ul.addClass('mybbcode-processed')
}
}
});
}
/**
* Добавление обработчиков пользовательских тегов, добавленных через систему пользовательских тегов
* (/admin_forms.php — "Пользовательские bb-теги"). Для корректной работы обработчика рекомендуется
* (но НЕ необходимо) указать для тега флаг `u` (unique).
* @param {string} name Строка, название тега. Должно совпадать с названием тега в поле "Пользовательские bb-теги".
* @param {TagCallback} callback Функция, которая будет вызываться по событию.
*/
addTag(name: string, callback: TagCallback): void {
if (this.tags[name]) {
console.warn('[MyBBCode] Tag "' + name + '" declared more than one time!');
}
this.tags[name] = callback;
}
/**
* Добавление общих обработчиков, которые будут применяться к сообщениям.
* @param {AdapterCallback} callback
*/
addAdapter(callback: AdapterCallback): void {
this.adapters.push(callback);
}
/**
* Добавление обработчика, изменяющего столбец автора сообщения.
* После обработки, к ul-элементу будет добавлен класс `mybbcode-processed`; если элемент имеет этот класс,
* обработчик не будет применён.
* @param {AuthorEditorCallback} callback
*/
addAuthorEditor(callback: AuthorEditorCallback): void {
this.authorEditors.push(callback);
}
/**
* Сгенерировать кнопку
* @param {string} key id кнопки
* @returns {string} Код кнопки
*/
private generateEditorButton(key: string): string {
if (!FORUM.editor) {
return '';
}
if (key in this.editor.specials) {
return this.editor.specials[key]
}
if (key in FORUM.editor) {
const value = FORUM.editor[key];
return this.editor.generator(key, value);
}
return '';
}
/**
* Сгенерировать заново строку с кнопками редактора.
*/
generateEditorButtons(): void {
if (!FORUM.editor || (this.editor.buttons === null)) {
return;
}
const old = Object.keys(FORUM.editor).filter((v) => v !== 'settings');
let changed = old.length !== this.editor.buttons.length;
for (let i = 0; !changed && (i < old.length); i++) {
changed = changed || (old[i] !== this.editor.buttons[i]);
}
if (!changed) {
return;
}
let result = this.editor.wrapper[0];
for (const key of this.editor.buttons) {
result += this.generateEditorButton(key);
}
result += this.editor.wrapper[1];
$('#form-buttons').empty().html(result);
}
/**
* Добавление кнопки формы.
* После объявления при помощи этого метода, объект `data` будет доступен после объявления как `FORUM.editor.key`.
* @param {string} key Идентификатор кнопки, который будет добавлен к `td.button#button-key`.
* @param {MybbEditorTag} data Данные поля, которые будут использоваться стандартными скриптами.
* Этому объектов можно указать и другие поля, однако указанные обязательны.
* @param {string} data.name Строка, название кнопки.
* @param {function} data.onclick Функция, которая будет вызвана при нажатии кнопки.
* @param {number|string|false} [before] Указание, перед каким элементом необходимо добавить кнопку. Допустимые значения:
* Число `i` — тогда кнопка будет добавлена перед `i`-м элементом массива `MyBBCode.editor.buttons`.
* Строкой-идентификатором кнопки `key` — тогда новая кнопка будет помещена перед кнопкой с идентификатором `key`.
*/
addEditorButton(key: string, data: MybbEditorTag, before: number|string|false = false): void {
if (!('FORUM' in window) || !('editor' in FORUM)) {
return null;
}
if (!(key in this.editor.specials)) {
FORUM.editor[key] = data;
}
if (this.editor.buttons !== null) {
let index: number|false;
if (before === false) {
index = false;
}
else if (typeof before === "string") {
index = this.editor.buttons.indexOf(before);
index = (index < 0) ? false : index;
}
else if ((typeof before === "number") && (before >= 0) && (before < this.editor.buttons.length)) {
index = before;
}
else {
index = false;
}
if (index === false) {
this.editor.buttons.push(key);
}
else {
this.editor.buttons.splice(index, 0, key);
}
}
}
}
+66
View File
@@ -0,0 +1,66 @@
declare module 'FORUM' {
let FORUM: {
editor: MybbEditor
}
export class MybbEditorTag {
/**
* @param {string} name Строка, название кнопки.
*/
name: string;
/**
* @param {function} onclick Функция, которая будет вызвана при нажатии кнопки.
*/
onclick: () => void;
}
export type MybbEditorSettings = {
autofocus: string
}
export type MybbEditor = {
settings: MybbEditorSettings;
[key: string]: MybbEditorTag | MybbEditorSettings;
}
export function quote(e: string, t: string, i: string): void;
export function bbcode(tagStart: string, tagEnd: string): void;
export function changeVisibility(areaName: string): void;
export function insert(value: string): void;
export function smile(emote: string): void;
export function tag_table(rows: number, columns: number): void;
export function MYBB_vsi(element: HTMLElement, event: any): void;
export function insertUploadedImage(element: HTMLElement | JQuery): void;
export function tag_spoiler(spoilerType: string): void;
export function keyboard(character: string): void;
export function to(target: string): void;
export class MYBB_vsc {
cH: any;
RG: { [key: string]: MYBB_vsc_item };
checkHost: (hostingItem: MYBB_vsc_item, input: string) => boolean;
clickHost: (element: HTMLAnchorElement) => void;
parse: () => void;
isLink: (input: string) => boolean;
message: (html: string) => void;
reInit: () => void;
}
type MYBB_vsc_item = {
l: "0" | "1";
t: string;
x: RegExp;
nf: string;
}
}
+10
View File
@@ -0,0 +1,10 @@
export type AdapterCallback = (container: JQuery) => void;
export type TagCallback = (elem: JQuery,
selector: string,
set_html: string | boolean) => void;
export type AuthorEditorCallback = (container: JQuery) => void;
export type EventResolver = (e: any) => JQuery
+19
View File
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"noImplicitAny": true,
"module": "es6",
"target": "es2017",
"allowJs": true,
"declaration": true,
"moduleResolution": "node",
"esModuleInterop": true,
"typeRoots": [
"./node_modules/@types",
"./src/types"
]
},
"files": [
"src/main.ts"
]
}
+77
View File
@@ -0,0 +1,77 @@
const path = require("path");
const webpack = require("webpack");
const BomPlugin = require("webpack-utf8-bom");
const { exec } = require("node:child_process");
const TerserPlugin = require("terser-webpack-plugin");
const isProduction = process.env.NODE_ENV === "production";
const config = {
entry: "./src/main.ts",
output: {
path: path.resolve(__dirname, "dist"),
filename: () => "mybbcode" + (isProduction ? ".min" : "") + ".js",
},
plugins: [
new BomPlugin(true),
{
apply: (compiler) => {
compiler.hooks.afterEmit.tap("AfterEmitPlugin", (compilation) => {
exec("./upload.sh", (err, stdout, stderr) => {
if (stdout) process.stdout.write(stdout);
if (stderr) process.stderr.write(stderr);
console.log("Uploaded.");
});
});
},
},
// Add your plugins here
// Learn more about plugins from https://webpack.js.org/configuration/plugins/
],
module: {
rules: [
{
test: /\.(ts|tsx)$/i,
loader: "ts-loader",
exclude: ["/node_modules/"],
},
{
test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif)$/i,
type: "asset",
},
{
test: /\.html$/i,
loader: "html-loader",
},
// Add your rules for custom modules here
// Learn more about loaders from https://webpack.js.org/loaders/
],
},
resolve: {
extensions: [".tsx", ".ts", ".jsx", ".js", "..."],
},
externals: {
jquery: "jQuery",
},
watchOptions: {
aggregateTimeout: 20,
},
optimization: {
minimize: isProduction,
minimizer: [new TerserPlugin({
terserOptions: {
compress: true,
},
minify: TerserPlugin.uglifyJsMinify,
})],
},
};
module.exports = () => {
if (isProduction) {
config.mode = "production";
} else {
config.mode = "development";
}
return config;
};