Merge branch 'master' into develop

This commit is contained in:
Barış Soner Uşaklı
2026-01-16 13:08:29 -05:00
13 changed files with 311 additions and 71 deletions

View File

@@ -1,3 +1,213 @@
#### v4.8.0 (2026-01-14)
##### Chores
* **deps:**
* update dependency @stylistic/eslint-plugin to v5.7.0 (#13879) (be0d43cf)
* update commitlint monorepo to v20.3.1 (#13876) (c88ce519)
* update dependency sass-embedded to v1.97.2 (#13870) (27d511ff)
* update commitlint monorepo to v20.3.0 (#13865) (447cfd03)
* update dependency smtp-server to v3.18.0 (#13858) (f35c77dd)
* update dependency jsdom to v27.4.0 (#13860) (37c052f4)
* update dependency sass-embedded to v1.97.1 (#13850) (d28866ab)
* update dependency sass-embedded to v1.97.0 (#13837) (168b6e63)
* update dependency smtp-server to v3.17.1 (#13829) (ad895efb)
* update dependency @eslint/js to v9.39.2 (#13830) (22fe83f0)
* update github artifact actions (#13831) (b1696218)
* update actions/cache action to v5 (#13828) (0fcc8543)
* update dependency smtp-server to v3.17.0 (#13824) (3adcbe0f)
* update dependency sass-embedded to v1.96.0 (#13821) (b992511b)
* update dependency sass-embedded to v1.95.1 (#13817) (a2f2c8c7)
* update dependency jsdom to v27.3.0 (#13814) (a35c326a)
* update commitlint monorepo to v20.2.0 (#13810) (e50edd52)
* update dependency lint-staged to v16.2.7 (#13785) (76b6b3b2)
* update actions/checkout action to v6 (#13802) (7f21a171)
* bump profile max upload size default (bed6ed3c)
* up themes (b323b5d8)
* up markdown (eb77c9bf)
* up mentions (648d9c78)
* incrementing version number - v4.7.2 (cd419d8a)
* update changelog for v4.7.2 (2f0526b8)
* incrementing version number - v4.7.1 (afb88805)
* allow direct testing in test/categories.js (29687722)
* incrementing version number - v4.7.0 (e82d40f8)
* incrementing version number - v4.6.3 (9fc5b0f3)
* incrementing version number - v4.6.2 (f98747db)
* incrementing version number - v4.6.1 (f47aa678)
* incrementing version number - v4.6.0 (ee395bc5)
* incrementing version number - v4.5.2 (ad2da639)
* incrementing version number - v4.5.1 (69f4b61f)
* incrementing version number - v4.5.0 (f05c5d06)
* incrementing version number - v4.4.6 (074043ad)
* incrementing version number - v4.4.5 (6f106923)
* incrementing version number - v4.4.4 (d323af44)
* incrementing version number - v4.4.3 (d354c2eb)
* incrementing version number - v4.4.2 (55c510ae)
* incrementing version number - v4.4.1 (5ae79b4e)
* incrementing version number - v4.4.0 (0a75eee3)
* incrementing version number - v4.3.2 (b92b5d80)
* incrementing version number - v4.3.1 (308e6b9f)
* incrementing version number - v4.3.0 (bff291db)
* incrementing version number - v4.2.2 (17fecc24)
* incrementing version number - v4.2.1 (852a270c)
* incrementing version number - v4.2.0 (87581958)
* incrementing version number - v4.1.1 (b2afbb16)
* incrementing version number - v4.1.0 (36c80850)
* incrementing version number - v4.0.6 (4a52fb2e)
* incrementing version number - v4.0.5 (1792a62b)
* incrementing version number - v4.0.4 (b1125cce)
* incrementing version number - v4.0.3 (2b65c735)
* incrementing version number - v4.0.2 (73fe5fcf)
* incrementing version number - v4.0.1 (a461b758)
* incrementing version number - v4.0.0 (c1eaee45)
##### Documentation Changes
* update openapi schema for missing routes related to crossposting (d81b644d)
##### New Features
* user crossposts federate as:Announce (273bc68c)
* add missing files, minor changes to crossposts list modal (38fd1798)
* introduce new front-end UI button for cross-posting, hide move on topics in remote cids (0041cfe2)
* disallow moving topics to and from remote categories, + basic tests for topic moving (ea1e4c7d)
* API v3 calls to crosspost and uncrosspost a topic to and from a category (74172ecc)
* refactor out.announce.topic to allow user announces, refactor tests to accommodate (874ffd7b)
* stop extraneous vote and tids_read data from being saved for remote users (097d0802)
* support remote Dislike activity, federate out a Dislike on downvote, bwahahah (528cd258)
* expand postingRestrictedToMods mask testing, handle actor update for that prop (6a561050)
* setAddBulk (#13805) (7d5402fe)
* save privilege masking set when asserting group (f0a7a442)
* patch low-level privilege query calls to accept privilege masks at the cid level (4020e1be)
* federate out topic removal activities when topic is deleted and purged from a local category (3ab61615)
##### Bug Fixes
* i18n fallbacks (a73ab8ee)
* #13889, custom emoji from Piefed (0c75934a)
* #13888, decode html entities for AP category name and description (6eea4df5)
* derp (bcc204fa)
* bump themes (a4c470ff)
* guard against negative uids crossposting (2f96eed4)
* bump themes (943b53b0)
* calling sortedSetRemove to remove multiple values, instead of baking it into sortedSetRemoveBulk (82507c0f)
* unused values (b9b33f9f)
* typo, client-side handling of crossposts as pertains to uncategorized topics (7465762d)
* client-side handling of category selector when cross-posting so only local cids are sent to backend (ea417b06)
* update category sync logic to utilise crossposts instead (e5ee52e5)
* remove old remote user to remote category migration logic + tests (28249efb)
* update auto-categorization rules to also handle already-categorized topics via crosspost (148663c5)
* topic crosspost delete and purge handling (f6cc556d)
* bug where privileges users could not uncrosspost others' crossposts. Tests (0a0a7da9)
* allow non-mods to crosspost, move crosspost button out of topic tools, in-modal state updates (6daaad81)
* removed ajaxify refresh on crosspost commit, dynamically update post stats in template, logic fix (b981082d)
* nodeinfo route to publish federation.enabled in metadata section (14aa2bee)
* bump link-preview again (74e47820)
* bump link-preview (486e77c7)
* remove commented out require (ffc3d279)
* bump link-preview (cc1649e0)
* auto-enable post queue as default, adjust tests to compensate (9390ccb6)
* remove bidiControls from notification.bodyShort (b0679cad)
* author of boosted content was not targeted in the activity (b05199d8)
* closes #13872, use translator.compile for notification text (5a031d01)
* #13715, dont reduce hardcap if usersPerPage is < 50 (cb31e70e)
* dont use sass-embedded on freebsd, #13867 (b7de0cc7)
* wrong increment value (20918b52)
* increment progress on upgrade script (8abe0dfa)
* add join-lemmy context for outgoing category group actors context prop (f1d50c35)
* use setsAdd (d8e55d58)
* missing await (4a6dcf1a)
* admin privilege overrides only apply to local categories (7b194c69)
* have notes.assert call out.announce.topic only if uid is set (so, if note assertion is called via search; manual pull) (3b7bcba6)
* deep clone activity prop before execution; feps.announce (977a67f4)
* minor comment fix (411baa21)
* publish `postingRestrictedToMods` property in group actor (c365c1dc)
* **deps:**
* update dependency spdx-license-list to v6.11.0 (#13890) (9b1c32b1)
* update dependency diff to v8.0.3 (#13882) (974ab1f8)
* update dependency nodebb-theme-persona to v14.1.23 (#13878) (47074b3c)
* update dependency nodebb-theme-harmony to v2.1.31 (#13877) (125c8e58)
* update dependency body-parser to v2.2.2 (#13873) (e717f00e)
* update dependency sass to v1.97.2 (#13871) (5100cc4f)
* update dependency nodebb-plugin-markdown to v13.2.3 (#13869) (a8c18f8a)
* update dependency nodebb-theme-harmony to v2.1.30 (#13863) (49379e2e)
* update dependency nodebb-theme-persona to v14.1.22 (#13864) (e4435e52)
* update dependency @isaacs/ttlcache to v2.1.4 (#13861) (89abdca1)
* update socket.io packages to v4.8.3 (#13857) (6807f860)
* update dependency sass to v1.97.1 (#13856) (7325b995)
* update dependency nodebb-theme-persona to v14.1.20 (#13855) (b8f68fb4)
* update dependency nodebb-theme-harmony to v2.1.28 (#13854) (f98fd6dc)
* update dependency fs-extra to v11.3.3 (#13851) (160ce17f)
* update dependency nodemailer to v7.0.12 (#13853) (f6ef041c)
* update dependency nodebb-plugin-2factor to v7.6.1 (#13852) (abcb2382)
* update dependency validator to v13.15.26 (#13846) (2a10f904)
* update dependency nodebb-theme-persona to v14.1.19 (#13849) (b933d1a2)
* update dependency nodebb-theme-harmony to v2.1.27 (#13848) (61d8cba9)
* update dependency webpack to v5.104.1 (#13847) (bb5a90a3)
* update dependency esbuild to v0.27.2 (#13842) (5844e393)
* update dependency nodebb-plugin-mentions to v4.8.4 (#13845) (2ffa4383)
* update dependency webpack to v5.104.0 (#13839) (f16eec30)
* update dependency sass to v1.97.0 (#13838) (ab8dbb41)
* update dependency fetch-cookie to v3.2.0 (#13836) (0ef5cbbb)
* update dependency autoprefixer to v10.4.23 (#13835) (7c2e8330)
* update dependency terser-webpack-plugin to v5.3.16 (#13827) (da7c9b32)
* update dependency sass to v1.96.0 (#13822) (d4f53a62)
* update dependency winston to v3.19.0 (#13812) (81c232f1)
* update dependency cron to v4.4.0 (#13818) (f077c4ca)
* update dependency sass to v1.95.1 (#13816) (adedb7b6)
* update dependency sass to v1.95.0 (#13815) (eaa6e71a)
* update dependency terser-webpack-plugin to v5.3.15 (#13811) (10d2e929)
* update dependency esbuild to v0.27.1 (#13806) (6b1dcb4b)
* update dependency jsonwebtoken to v9.0.3 (#13807) (7b734cfd)
* update dependency ace-builds to v1.43.5 (#13797) (93057306)
* update dependency lru-cache to v11.2.4 (#13798) (731933a6)
* update dependency express to v4.22.1 (#13800) (38321220)
* update dependency ipaddr.js to v2.3.0 (#13801) (ad5cd27b)
* update dependency nodemailer to v7.0.11 (#13799) (ecec1f45)
* update dependency cron to v4.3.5 (#13796) (5ba6bea0)
* update dependency body-parser to v2.2.1 (#13795) (624ef616)
* update dependency @isaacs/ttlcache to v2.1.3 (#13791) (5f55ca85)
* update dependency sass to v1.94.2 (#13786) (1cb8b381)
* update dependency redis to v5.10.0 (#13787) (1bcfe3f0)
##### Other Changes
* fix... tests (d20906b5)
* still broken... more debug logs (a82e1f44)
* log mock results (8236b594)
##### Refactors
* check if tid is truthy (0e1ccfc9)
* crossposts.get to return limited category data (name, icon, etc.), fixed up crosspost modal to hide uncategorized and all categories options (349b0875)
* move crosspost methods into their own file in src/topics (1be88ca0)
* silence if-function deprecation on prod (403230cc)
* clear quick reply as soon as submitting (a331f8da)
##### Tests
* intify uid/cid if they are numbers (when getting crossposts) (47e37ed5)
* stop using partialDeepStrictEqual for now (0677689a)
* ensure auto-cat and cat sync logic properly integrates with crossposts (add163a4)
* crossposting behaviour and logic tests (947676ef)
* new test file for crossposts (3560b6a3)
* additional logic to allow multi-typing in schema type (4f1fa2d1)
* lowercase tags (81cac015)
* fix test to check for Secure in cookie string if test runner domain is https (5954015e)
* more out.announce tests (cfdbbb04)
* basic tests for activitypub.out (67912dc9)
* update activitypub._sent to save targets as well, updated tests to accommodate format change (41368ef8)
* test runs should not actually federate activities out (483ab083)
* check if tests pass without await (5414cf47)
* add back logs for failing test (301b5386)
* add a test for set db.exists (#13809) (69562704)
* fix failing test by adjusting the tests (c5292442)
* privilege masking tests (934e6be9)
* log label (22d3c523)
* log activities (e39c9149)
* on test fail show activities (841bd825)
* new mongodb deps (#13793) (287b2569)
#### v4.7.2 (2025-12-24)
##### Chores

View File

@@ -2,7 +2,7 @@
"name": "nodebb",
"license": "GPL-3.0",
"description": "NodeBB Forum",
"version": "4.7.2",
"version": "4.8.0",
"homepage": "https://www.nodebb.org",
"repository": {
"type": "git",
@@ -98,7 +98,7 @@
"nconf": "0.13.0",
"nodebb-plugin-2factor": "7.6.1",
"nodebb-plugin-composer-default": "10.3.1",
"nodebb-plugin-dbsearch": "6.3.4",
"nodebb-plugin-dbsearch": "6.3.5",
"nodebb-plugin-emoji": "6.0.5",
"nodebb-plugin-emoji-android": "4.1.1",
"nodebb-plugin-link-preview": "2.2.1",
@@ -107,7 +107,7 @@
"nodebb-plugin-spam-be-gone": "2.3.2",
"nodebb-plugin-web-push": "0.7.6",
"nodebb-rewards-essentials": "1.0.2",
"nodebb-theme-harmony": "2.1.33",
"nodebb-theme-harmony": "2.1.35",
"nodebb-theme-lavender": "7.1.19",
"nodebb-theme-peace": "2.2.49",
"nodebb-theme-persona": "14.1.25",
@@ -203,4 +203,4 @@
"url": "https://github.com/barisusakli"
}
]
}
}

View File

@@ -88,7 +88,7 @@ define('admin/manage/group', [
bootbox.confirm('[[admin/manage/groups:alerts.confirm-delete]]', function (confirm) {
if (confirm) {
api.del(`/groups/${slugify(ajaxify.data.group.name)}`, {}).then(() => {
ajaxify.go('/admin/managegroups');
ajaxify.go('/admin/manage/groups');
}).catch(alerts.error);
}
});

View File

@@ -40,7 +40,6 @@ define('admin/manage/groups', [
const createModal = $('#create-modal');
const createGroupName = $('#create-group-name');
const createModalGo = $('#create-modal-go');
const createModalError = $('#create-modal-error');
createGroupName.trigger('focus');
createModal.on('keypress', function (e) {
@@ -61,18 +60,12 @@ define('admin/manage/groups', [
};
api.post('/groups', submitObj).then((response) => {
createModalError.addClass('hide');
createGroupName.val('');
createModal.on('hidden.bs.modal', function () {
ajaxify.go('admin/manage/groups/' + response.name);
});
createModal.modal('hide');
}).catch((err) => {
if (!utils.hasLanguageKey(err.status.message)) {
err.status.message = '[[admin/manage/groups:alerts.create-failure]]';
}
createModalError.translateHtml(err.status.message).removeClass('hide');
});
}).catch(alerts.error);
});
});
});

View File

@@ -8,13 +8,16 @@ define('forum/groups/list', [
Groups.init = function () {
// Group creation
$('button[data-action="new"]').on('click', function () {
bootbox.prompt('[[groups:new-group.group-name]]', function (name) {
if (name && name.length) {
api.post('/groups', {
name: name,
}).then((res) => {
const modal = bootbox.prompt('[[groups:new-group.group-name]]', function (name) {
if (name === '') {
return false;
}
if (name && name.trim().length) {
api.post('/groups', { name }).then((res) => {
modal.modal('hide');
ajaxify.go('groups/' + res.slug);
}).catch(alerts.error);
return false;
}
});
});
@@ -42,19 +45,17 @@ define('forum/groups/list', [
return false;
};
function renderSearchResults(data) {
app.parseAndTranslate('partials/paginator', {
pagination: data.pagination,
}).then(function (html) {
$('.pagination-container').replaceWith(html);
});
const groupsEl = $('#groups-list');
app.parseAndTranslate('partials/groups/list', {
groups: data.groups,
}).then(function (html) {
groupsEl.empty().append(html);
});
async function renderSearchResults(data) {
const [paginationHtml, groupsHtml] = await Promise.all([
app.parseAndTranslate('partials/paginator', {
pagination: data.pagination,
}),
app.parseAndTranslate('partials/groups/list', {
groups: data.groups,
}),
]);
$('.pagination-container').replaceWith(paginationHtml);
$('#groups-list').empty().append(groupsHtml);
}
return Groups;

View File

@@ -464,7 +464,7 @@ module.exports = function (utils, load, warn) {
*/
Translator.escape = function escape(text) {
return typeof text === 'string' ?
text.replace(/\[\[/g, '&lsqb;&lsqb;').replace(/\]\]/g, '&rsqb;&rsqb;') :
text.replace(/\[\[([a-zA-Z0-9_.-]+:[a-zA-Z0-9_.-]+)\]\]/g, '&lsqb;&lsqb;$1&rsqb;&rsqb;') :
text;
};

View File

@@ -96,10 +96,14 @@ module.exports = function (Categories) {
};
async function getTopics(tids, uid) {
const topicData = await topics.getTopicsFields(
tids,
['tid', 'mainPid', 'slug', 'title', 'teaserPid', 'cid', 'postcount']
);
const [topicData, crossposts] = await Promise.all([
topics.getTopicsFields(
tids,
['tid', 'mainPid', 'slug', 'title', 'teaserPid', 'cid', 'postcount']
),
topics.crossposts.get(tids),
]);
topicData.forEach((topic) => {
if (topic) {
topic.teaserPid = topic.teaserPid || topic.mainPid;
@@ -124,6 +128,7 @@ module.exports = function (Categories) {
slug: topicData[index].slug,
title: topicData[index].title,
};
teaser.crossposts = crossposts[index];
}
});
return teasers.filter(Boolean);
@@ -132,15 +137,20 @@ module.exports = function (Categories) {
function assignTopicsToCategories(categories, topics) {
categories.forEach((category) => {
if (category) {
category.posts = topics.filter(
t => t.cid &&
(t.cid === category.cid || (t.parentCids && t.parentCids.includes(category.cid)))
)
category.posts = topics.filter(t =>
t.cid &&
(t.cid === category.cid ||
(t.parentCids && t.parentCids.includes(category.cid)) ||
(t.crossposts.some(({ cid }) => parseInt(cid, 10) === category.cid))
))
.sort((a, b) => b.timestamp - a.timestamp)
.slice(0, parseInt(category.numRecentReplies, 10));
}
});
topics.forEach((t) => { t.parentCids = undefined; });
topics.forEach((t) => {
t.parentCids = undefined;
t.crossposts = undefined;
});
}
function bubbleUpChildrenPosts(categoryData) {

View File

@@ -40,6 +40,7 @@ helpers.noScriptErrors = async function (req, res, error, httpStatus) {
};
helpers.terms = {
alltime: 'alltime',
daily: 'day',
weekly: 'week',
monthly: 'month',
@@ -101,7 +102,7 @@ helpers.buildFilters = function (url, filter, query) {
helpers.buildTerms = function (url, term, query) {
return [{
name: '[[recent:alltime]]',
url: url + helpers.buildQueryString(query, 'term', ''),
url: url + helpers.buildQueryString(query, 'term', 'alltime'),
selected: term === 'alltime',
term: 'alltime',
}, {

View File

@@ -1,5 +1,6 @@
'use strict';
const _ = require('lodash');
const db = require('../database');
const topics = require('.');
const user = require('../user');
@@ -10,30 +11,39 @@ const utils = require('../utils');
const Crossposts = module.exports;
Crossposts.get = async function (tid) {
const crosspostIds = await db.getSortedSetMembers(`tid:${tid}:crossposts`);
let crossposts = await db.getObjects(crosspostIds.map(id => `crosspost:${id}`));
const cids = crossposts.reduce((cids, crossposts) => {
cids.add(crossposts.cid);
return cids;
}, new Set());
let categoriesData = await categories.getCategoriesFields(
Array.from(cids), ['cid', 'name', 'icon', 'bgColor', 'color', 'slug']
Crossposts.get = async function (tids) {
const isArray = Array.isArray(tids);
if (!isArray) {
tids = [tids];
}
const crosspostIds = await db.getSortedSetsMembers(tids.map(tid => `tid:${tid}:crossposts`));
const allCrosspostIds = crosspostIds.flat();
const allCrossposts = await db.getObjects(allCrosspostIds.map(id => `crosspost:${id}`));
const categoriesData = await categories.getCategoriesFields(
_.uniq(allCrossposts.map(c => c.cid)), ['cid', 'name', 'icon', 'bgColor', 'color', 'slug']
);
categoriesData = categoriesData.reduce((map, category) => {
const categoriesMap = categoriesData.reduce((map, category) => {
map.set(parseInt(category.cid, 10), category);
return map;
}, new Map());
crossposts = crossposts.map((crosspost, idx) => {
crosspost.id = crosspostIds[idx];
crosspost.category = categoriesData.get(parseInt(crosspost.cid, 10));
crosspost.uid = utils.isNumber(crosspost.uid) ? parseInt(crosspost.uid) : crosspost.uid;
crosspost.cid = utils.isNumber(crosspost.cid) ? parseInt(crosspost.cid) : crosspost.cid;
return crosspost;
});
const crosspostMap = allCrossposts.reduce((map, crosspost, index) => {
const id = allCrosspostIds[index];
if (id && crosspost) {
map.set(id, crosspost);
crosspost.id = id;
crosspost.category = categoriesMap.get(parseInt(crosspost.cid, 10));
crosspost.uid = utils.isNumber(crosspost.uid) ? parseInt(crosspost.uid, 10) : crosspost.uid;
crosspost.cid = utils.isNumber(crosspost.cid) ? parseInt(crosspost.cid, 10) : crosspost.cid;
}
return map;
}, new Map());
return crossposts;
const crossposts = crosspostIds.map(ids => ids.map(id => crosspostMap.get(id)));
return isArray ? crossposts : crossposts[0];
};
Crossposts.add = async function (tid, cid, uid) {

View File

@@ -6,7 +6,6 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-hidden="true"></button>
</div>
<div class="modal-body">
<div class="alert alert-danger hide" id="create-modal-error"></div>
<form>
<div class="mb-3">
<label class="form-label" for="create-group-name">[[admin/manage/groups:name]]</label>

View File

@@ -159,9 +159,9 @@ describe('Crossposting (& related logic)', () => {
it('should not let another user uncrosspost', async () => {
const uid2 = await user.create({ username: utils.generateUUID().slice(0, 8) });
assert.rejects(
await assert.rejects(
topics.crossposts.remove(tid, cid2, uid2),
'[[error:invalid-data]]',
{ message: '[[error:invalid-data]]' },
);
});
@@ -184,9 +184,9 @@ describe('Crossposting (& related logic)', () => {
});
it('should throw on uncrossposting if already uncrossposted', async () => {
assert.rejects(
await assert.rejects(
topics.crossposts.remove(tid, cid2, uid),
'[[error:invalid-data]]',
{ message: '[[error:invalid-data]]' },
);
});
});
@@ -286,9 +286,9 @@ describe('Crossposting (& related logic)', () => {
it('should fail to uncrosspost if not mod of passed-in category', async () => {
await privileges.categories.give(['moderate'], cid1, [privUid]);
assert.rejects(
await assert.rejects(
topics.crossposts.remove(tid, cid2, privUid),
'[[error:invalid-data]]',
{ message: '[[error:invalid-data]]' },
);
});

View File

@@ -87,22 +87,22 @@ describe('Topic tools', () => {
});
it('should throw when attempting to move a topic from a remote category', async () => {
assert.rejects(
await assert.rejects(
topics.tools.move(tid1, {
cid: localCid,
uid: 'system',
}),
'[[error:cant-move-topic-to-from-remote-categories]]'
{ message: '[[error:no-topic]]' }
);
});
it('should throw when attempting to move a topic to a remote category', async () => {
assert.rejects(
await assert.rejects(
topics.tools.move(tid2, {
cid: remoteCid,
uid: 'system',
}),
'[[error:cant-move-topic-to-from-remote-categories]]'
{ message: '[[error:cant-move-topic-to-from-remote-categories]]' }
);
});
});

View File

@@ -334,6 +334,22 @@ describe('Translator static methods', () => {
);
done();
});
it('should escape all translation patterns within text', (done) => {
assert.strictEqual(
Translator.escape('some nice text [[global:home]] here and [[global:search]] there'),
'some nice text &lsqb;&lsqb;global:home&rsqb;&rsqb; here and &lsqb;&lsqb;global:search&rsqb;&rsqb; there'
);
done();
});
it('should not escape markdown links', (done) => {
assert.strictEqual(
Translator.escape('[link text [test]](https://example.org)'),
'[link text [test]](https://example.org)'
);
done();
});
});
describe('.unescape', () => {