feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
这个提交包含在:
@@ -0,0 +1,148 @@
|
||||
import { unique } from '@joplin/lib/ArrayUtils';
|
||||
import { attributesHtml, isSelfClosingTag } from '@joplin/renderer/htmlUtils';
|
||||
import { Translations } from '../../utils/translation';
|
||||
import { AllHtmlEntities as Entities } from 'html-entities';
|
||||
const htmlentities = new Entities().encode;
|
||||
import * as htmlparser2 from '@joplin/fork-htmlparser2';
|
||||
|
||||
const trimHtml = (content: string) => {
|
||||
return content
|
||||
.replace(/\n/g, '')
|
||||
.replace(/^(&tab;)+/i, '')
|
||||
.replace(/^( )+/i, '')
|
||||
.replace(/(&tab;)+$/i, '')
|
||||
.replace(/( )+$/i, '')
|
||||
.replace(/^\t+/, '')
|
||||
.replace(/\t+$/, '');
|
||||
};
|
||||
|
||||
const findTranslation = (englishString: string, translations: Translations): string => {
|
||||
const stringsToTry = unique([
|
||||
englishString,
|
||||
englishString.replace(/<br\/>/gi, '<br>'),
|
||||
englishString.replace(/<br \/>/gi, '<br>'),
|
||||
englishString
|
||||
.replace(/'/gi, '\'')
|
||||
.replace(/"/gi, '"'),
|
||||
]) as string[];
|
||||
|
||||
for (const stringToTry of stringsToTry) {
|
||||
// Note that we don't currently support plural forms for the website
|
||||
if (translations[stringToTry] && translations[stringToTry].length) return translations[stringToTry][0];
|
||||
}
|
||||
|
||||
return englishString;
|
||||
};
|
||||
|
||||
const encodeHtml = (decodedText: string): string => {
|
||||
return htmlentities(decodedText)
|
||||
.replace(/	/gi, '\t')
|
||||
.replace(/{{> /gi, '{{> '); // Don't break Mustache partials
|
||||
};
|
||||
|
||||
export default (html: string, _languageCode: string, translations: Translations) => {
|
||||
const output: string[] = [];
|
||||
|
||||
interface State {
|
||||
// When inside a block that needs to be translated, this array
|
||||
// accumulates the opening tags. For example, this text:
|
||||
//
|
||||
// <div translate>Hello <b>world</b></div>
|
||||
//
|
||||
// will have the tags ['div', 'b']
|
||||
//
|
||||
// This is used to track when we've processed all the content, including
|
||||
// HTML content, within a translatable block. Once that stack is empty,
|
||||
// we reached the end, and can translate the string that we got.
|
||||
translateStack: string[];
|
||||
|
||||
// Keep a reference to the opening tag. For example in:
|
||||
//
|
||||
// <div translate>Hello <b>world</b></div>
|
||||
//
|
||||
// The opening tag is "div".
|
||||
currentTranslationTag: string[];
|
||||
|
||||
// Once we finished processing the translatable block, this will contain
|
||||
// the string to be translated. It may contain HTML.
|
||||
currentTranslationContent: string[];
|
||||
|
||||
// Tells if we're at the beginning of a translatable block.
|
||||
translateIsOpening: boolean;
|
||||
|
||||
inScript: boolean;
|
||||
}
|
||||
|
||||
const state: State = {
|
||||
translateStack: [],
|
||||
currentTranslationTag: [],
|
||||
currentTranslationContent: [],
|
||||
translateIsOpening: false,
|
||||
inScript: false,
|
||||
};
|
||||
|
||||
const pushContent = (state: State, content: string) => {
|
||||
if (state.translateStack.length) {
|
||||
if (state.translateIsOpening) {
|
||||
state.currentTranslationTag.push(content);
|
||||
} else {
|
||||
state.currentTranslationContent.push(content);
|
||||
}
|
||||
} else {
|
||||
output.push(content);
|
||||
}
|
||||
};
|
||||
|
||||
const parser = new htmlparser2.Parser({
|
||||
|
||||
onopentag: (name: string, attrs: Record<string, string>) => {
|
||||
if (name === 'script') state.inScript = true;
|
||||
|
||||
if ('translate' in attrs) {
|
||||
if (state.translateStack.length) throw new Error(`Cannot have a translate block within another translate block. At tag "${name}" attrs: ${JSON.stringify(attrs)}`);
|
||||
state.translateStack.push(name);
|
||||
state.currentTranslationContent = [];
|
||||
state.currentTranslationTag = [];
|
||||
state.translateIsOpening = true;
|
||||
} else if (state.translateStack.length) {
|
||||
state.translateStack.push(name);
|
||||
}
|
||||
|
||||
let attrHtml = attributesHtml(attrs);
|
||||
if (attrHtml) attrHtml = ` ${attrHtml}`;
|
||||
const closingSign = isSelfClosingTag(name) ? '/>' : '>';
|
||||
|
||||
pushContent(state, `<${name}${attrHtml}${closingSign}`);
|
||||
state.translateIsOpening = false;
|
||||
},
|
||||
|
||||
ontext: (decodedText: string) => {
|
||||
const encodedText = state.inScript ? decodedText : encodeHtml(decodedText);
|
||||
pushContent(state, encodedText);
|
||||
},
|
||||
|
||||
onclosetag: (name: string) => {
|
||||
if (state.translateStack.length) {
|
||||
state.translateStack.pop();
|
||||
|
||||
if (!state.translateStack.length) {
|
||||
const stringToTranslate = trimHtml(state.currentTranslationContent.join(''));
|
||||
const translation = findTranslation(stringToTranslate, translations);
|
||||
output.push(state.currentTranslationTag[0]);
|
||||
output.push(translation);
|
||||
}
|
||||
}
|
||||
|
||||
if (name === 'script') state.inScript = false;
|
||||
|
||||
if (isSelfClosingTag(name)) return;
|
||||
pushContent(state, `</${name}>`);
|
||||
},
|
||||
|
||||
}, { decodeEntities: true });
|
||||
|
||||
parser.write(html);
|
||||
parser.end();
|
||||
|
||||
return output.join('\n');
|
||||
};
|
||||
在新工单中引用
屏蔽一个用户