diff --git a/.gitignore b/.gitignore index ee733a56..5a016301 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,12 @@ out node_modules -VERSION *.vsix ROADMAP.md *.log /.vscode-test dist -tmp \ No newline at end of file +tmp + +# The welcome materials +/welcome/** +/changes.md diff --git a/.vscodeignore b/.vscodeignore index 59be4d39..cc0d66f8 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -13,8 +13,10 @@ src/ # Configuration files .github/ .vscode/ -VERSION .gitignore tsconfig.json package-json.lock webpack.config.js + +# The welcome flag file +welcome/WELCOMED diff --git a/package.json b/package.json index f8722d98..bf2d625f 100644 --- a/package.json +++ b/package.json @@ -439,7 +439,7 @@ ] }, "scripts": { - "vscode:prepublish": "webpack --mode production", + "vscode:prepublish": "node ./tools/prepublish.js", "compile": "webpack --mode none", "watch": "webpack --mode none --watch", "test": "tsc -p ./ && node ./out/test/runTest.js", diff --git a/package.nls.ja.json b/package.nls.ja.json index 09360eee..791c2148 100644 --- a/package.nls.ja.json +++ b/package.nls.ja.json @@ -15,7 +15,6 @@ "config.toc.unorderedList.marker.description": "目次(TOC)に `-` `*` `+` のどれを使用するか (順序なしリストの場合)", "config.toc.plaintext.description": "目次(TOC)にリンクなし(プレーンテキスト)を使用する", "config.toc.updateOnSave.description": "目次(TOC)を保存時に自動更新", - "config.toc.githubCompatibility.description": "目次(TOC)のGitHub互換", "config.toc.downcaseLink.description": "目次(TOC)用リンクを小文字化する", "config.toc.omittedFromToc.description": "プロジェクトファイルの目次(TOC)で除外する見出しの一覧(例. {\"README.md\": [\"# Introduction\"]})", "config.list.indentationSize.description": "異なる構文で異なるインデント幅を使用するかどうか(これは、生成された目次・TOCにも影響します)", @@ -34,23 +33,13 @@ "config.print.onFileSave.description": "ファイル保存時に現在のドキュメントをHTMLへ出力する", "config.print.validateUrls.description": "出力時のURL検証の有効化/無効化", "config.print.theme": "出力されたHTMLのテーマ", - "config.syntax.decorations.description": "Add syntax decorations", - "config.syntax.plainTheme.description": "`extension.syntax.decorations`が有効な場合にのみ作用させる", "config.katex.macros.description": "ユーザ定義のKaTeXマクロ", "config.completion.root": "Pathの自動補完におけるルートフォルダ", - "showMe": "表示する", - "dismiss": "却下", - "noValidMarkdownFile": "有効なMarkdownファイルがありません", - "printing": "出力中", - "to": "へ", - "unableToReadFile": "ファイルを読み取れません", - "revertingToImagePaths": "base64エンコードを画像Pathに戻す", - "customStyle": "カスタムスタイル", - "notFound": "が見つかりません。", - "cannotUseBuiltinSlugifyFunc": "VSCode組み込みのslugify関数が使用できません。GitHubのslugify関数にフォールバックします。VSCodeを最新バージョンに更新するか、 `githubCompatibility`を`true`に設定してください", - "1.3.0 msg": "エキサイティングな機能をご紹介! 自動で番号再割り当てできる順序付きリスト。", - "1.4.0 msg": "『Markdown-All-in-One v1.4.0』におけるたくさんの新機能", - "1.5.0 msg": "お久しぶりです。『Markdown-All-in-One v1.5.0』へようこそ。", - "2.1.0 msg": "『Markdown All in One』v2.1.0! リンクとしてURLを貼り付けしたり、マルチカーソルのサポートなど。", - "2.4.0 msg": "『Markdown All in One』v2.4.0! 新コマンド`toggleList`とKaTeXマクロのサポート。" + "ui.exporting.messageCustomCssNotFound": "'{0}' が見つかりません。", + "ui.exporting.messageExportingInProgress": "{1} 出力中: '{0}'", + "ui.exporting.messageRevertingToImagePaths": "base64エンコードを画像Pathに戻す。", + "ui.general.messageNoValidMarkdownFile": "有効な Markdown ファイルがありません。", + "ui.general.messageUnableToReadFile": "ファイルを読み取れません: '{0}'", + "ui.welcome.buttonDismiss": "却下", + "ui.welcome.buttonOpenLocal": "表示する" } diff --git a/package.nls.json b/package.nls.json index 722425be..50103f4e 100644 --- a/package.nls.json +++ b/package.nls.json @@ -45,20 +45,11 @@ "config.toc.slugifyMode.description": "The method to generate heading ID. This affects **links to headings** in **TOC**, **code completion**, and **printing**.", "config.toc.unorderedList.marker.description": "Use `-`, `*`, or `+` in the table of contents (for **unordered** list).", "config.toc.updateOnSave.description": "Auto update TOC on save.", - "showMe": "Show Me", - "dismiss": "Dismiss", - "noValidMarkdownFile": "No valid Markdown file", - "printing": "Printing", - "to": "to", - "unableToReadFile": "Unable to read file", - "revertingToImagePaths": "Reverting to image paths instead of base64 encoding", - "customStyle": "Custom style", - "notFound": " not found.", - "cannotUseBuiltinSlugifyFunc": "Cannot use VSCode built-in slugify function, fall back to GitHub slugify funtion. Try to update VSCode to the latest version or set setting `githubCompatibility` to `true`", - "1.3.0 msg": "Introduce an exciting feature! Auto renumbering ordered list.", - "1.4.0 msg": "Many new features of Markdown-All-in-One v1.4.0", - "1.5.0 msg": "Long time no see. Welcome to Markdown-All-in-One v1.5.0.", - "2.1.0 msg": "Markdown All in One v2.1.0! Paste URL as link, multi-cursor support, and more.", - "2.4.0 msg": "Markdown All in One v2.4.0! New command 'toggleList' and KaTeX macros support.", - "3.0.0 msg": "Markdown All in One v3.0.0! New command 'add section numbers' and better compatibility with other Markdown syntax extensions." + "ui.exporting.messageCustomCssNotFound": "Custom CSS '{0}' not found.", + "ui.exporting.messageExportingInProgress": "Printing '{0}' to {1} ...", + "ui.exporting.messageRevertingToImagePaths": "Reverting to image paths instead of base64 encoding.", + "ui.general.messageNoValidMarkdownFile": "No valid Markdown file.", + "ui.general.messageUnableToReadFile": "Unable to read file '{0}'.", + "ui.welcome.buttonDismiss": "Dismiss", + "ui.welcome.buttonOpenLocal": "Read" } \ No newline at end of file diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index 90f1e907..706ccfa3 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -37,20 +37,11 @@ "config.toc.slugifyMode.description": "生成标题 ID 的方法。该设置影响**目录**、**代码自动补全**、**打印**中**指向标题的链接**。", "config.toc.unorderedList.marker.description": "在目录中使用 `-`,`*` 或 `+` (仅对于**无序**列表)。", "config.toc.updateOnSave.description": "保存时自动更新目录。", - "showMe": "显示更新日志", - "dismiss": "忽略", - "noValidMarkdownFile": "未选中有效的 Markdown 文件", - "printing": "将", - "to": "打印到", - "unableToReadFile": "无法读取文件", - "revertingToImagePaths": "已使用图像路径而不是 base64 编码", - "customStyle": "自定义样式", - "notFound": "未找到", - "cannotUseBuiltinSlugifyFunc": "无法使用 VSCode 内置的 slugify 函数,已使用 GitHub 的 slugify 函数替代。请尝试更新 VSCode 到最新版本,或将 `githubCompatibility` 设置为 `true`", - "1.3.0 msg": "介绍一个令人兴奋的功能!自动为有序列表重新编号", - "1.4.0 msg": "Markdown-All-in-One v1.4.0 带来很多新功能", - "1.5.0 msg": "好久不见!欢迎使用 Markdown-All-in-One v1.5.0", - "2.1.0 msg": "Markdown All in One v2.1.0! 直接粘贴链接,多光标支持,以及更多新功能", - "2.4.0 msg": "Markdown All in One v2.4.0! 新功能「触发列表」,KaTeX 宏支持", - "3.0.0 msg": "Markdown All in One v3.0.0! 新功能「添加章节序号」,以及更好的与其它语法扩展的兼容性" + "ui.exporting.messageCustomCssNotFound": "自定义样式 '{0}' 未找到。", + "ui.exporting.messageExportingInProgress": "将 '{0}' 打印到 {1} …", + "ui.exporting.messageRevertingToImagePaths": "已使用图像路径而不是 base64 编码。", + "ui.general.messageNoValidMarkdownFile": "未选中有效的 Markdown 文件。", + "ui.general.messageUnableToReadFile": "无法读取文件 '{0}'。", + "ui.welcome.buttonDismiss": "忽略", + "ui.welcome.buttonOpenLocal": "阅读" } \ No newline at end of file diff --git a/src/contract/VisualStudioCodeLocaleId.ts b/src/contract/VisualStudioCodeLocaleId.ts new file mode 100644 index 00000000..a0b66cc3 --- /dev/null +++ b/src/contract/VisualStudioCodeLocaleId.ts @@ -0,0 +1,26 @@ +"use strict"; + +/** + * Visual Studio Code Locale ID. + * @see + * @see + */ +const enum VisualStudioCodeLocaleId { + Bulgarian = "bg", + ChineseSimplified = "zh-cn", + ChineseTraditional = "zh-tw", + Czech = "cs", + English = "en", + French = "fr", + German = "de", + Hungarian = "hu", + Italian = "it", + Japanese = "ja", + Korean = "ko", + PortugueseBrazil = "pt-br", + Russian = "ru", + Spanish = "es", + Turkish = "tr", +} + +export default VisualStudioCodeLocaleId; diff --git a/src/extension.ts b/src/extension.ts index 6bfdba2a..25beffcb 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,20 +1,19 @@ 'use strict'; -import * as fs from 'fs'; -import * as path from 'path'; -import { ExtensionContext, languages, window, workspace } from 'vscode'; +import { ExtensionContext, languages, Uri, window, workspace } from 'vscode'; import * as completion from './completion'; import * as formatting from './formatting'; import * as listEditing from './listEditing'; -import localize from './localize'; +import { config as configNls, localize } from './nls'; +import resolveResource from "./nls/resolveResource"; import * as preview from './preview'; import * as print from './print'; import * as decorations from './syntaxDecorations'; import * as tableFormatter from './tableFormatter'; import * as toc from './toc'; -import { getNewFeatureMsg, showChangelog } from './util'; export function activate(context: ExtensionContext) { + configNls({ extensionContext: context }); activateMdExt(context); if (workspace.getConfiguration('markdown.extension.math').get('enabled')) { @@ -64,37 +63,53 @@ function activateMdExt(context: ExtensionContext) { wordPattern: /(-?\d*\.\d\w*)|([^\!\@\#\%\^\&\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s\,\。\《\》\?\;\:\‘\“\’\”\(\)\【\】\、]+)/g }); - newVersionMessage(context.extensionPath); + showWelcome(context); } -function newVersionMessage(extensionPath: string) { - let data: string, currentVersion: string; +/** + * Shows a welcome message on first time startup. + */ +async function showWelcome(context: ExtensionContext): Promise { + const welcomeDirUri = Uri.joinPath(context.extensionUri, "welcome"); + + // The directory for an extension is recreated every time VS Code installs it. + // Thus, we only need to read and write an empty flag file there. + // If the file exists, then it's not the first time, and we don't need to do anything. + const flagFileUri = Uri.joinPath(welcomeDirUri, "WELCOMED"); try { - data = fs.readFileSync(`${extensionPath}${path.sep}package.json`).toString(); - currentVersion = JSON.parse(data).version; - if ( - fs.existsSync(`${extensionPath}${path.sep}VERSION`) - && fs.readFileSync(`${extensionPath}${path.sep}VERSION`).toString() === currentVersion - ) { - return; - } - fs.writeFileSync(`${extensionPath}${path.sep}VERSION`, currentVersion); - } catch (error) { - console.log(error); + await workspace.fs.stat(flagFileUri); return; + } catch { + workspace.fs.writeFile(flagFileUri, new Uint8Array()).then(() => { }, () => { }); } - const featureMsg = getNewFeatureMsg(currentVersion); - if (featureMsg === undefined) return; - const message1 = localize("showMe"); - const message2 = localize("dismiss"); - window.showInformationMessage(featureMsg, message1, message2).then(option => { - switch (option) { - case message1: - showChangelog(); - case message2: - break; + + // The existence of welcome materials depends on build options we set during pre-publish. + // If any condition is not met, then we don't need to do anything. + try { + // Confirm the message is valid. + // `locale` should be a string. But here we keep it `any` to suppress type checking. + const locale: any = JSON.parse(process.env.VSCODE_NLS_CONFIG as string).locale; + const welcomeMessageFileUri = Uri.file(resolveResource(welcomeDirUri.fsPath, "", ".txt", [locale, "en"], "")![0]); + const msgWelcome = Buffer.from(await workspace.fs.readFile(welcomeMessageFileUri)).toString("utf8"); + if (/^\s*$/.test(msgWelcome) || /\p{C}/u.test(msgWelcome)) { + return; } - }); + + // Confirm the file exists. + const changelogFileUri = Uri.joinPath(context.extensionUri, "changes.md"); + await workspace.fs.stat(changelogFileUri); + + const btnDismiss = localize("ui.welcome.buttonDismiss"); + const btnOpenLocal = localize("ui.welcome.buttonOpenLocal"); + + window.showInformationMessage(msgWelcome, btnOpenLocal, btnDismiss).then(selection => { + switch (selection) { + case btnOpenLocal: + workspace.openTextDocument(changelogFileUri).then(window.showTextDocument); + return; + } + }); + } catch { } } export function deactivate() { } diff --git a/src/localize.ts b/src/localize.ts deleted file mode 100644 index 5ece6fb8..00000000 --- a/src/localize.ts +++ /dev/null @@ -1,99 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import { extensions } from "vscode"; - -interface IConfig { - locale?: string; -} - -interface ILanguagePack { - [key: string]: string; -} - -export class Localize { - // get language pack when the instance be created - private bundle = this.resolveLanguagePack(); - constructor(private config: IConfig = {}) { } - /** - * translate the key - * @param key - * @param args - */ - public localize(key: string, ...args: any[]) { - const languagePack = this.bundle; - const message: string = languagePack[key] || key; - return this.format(message, args); - } - /** - * format the message - * @param message - * @param args - */ - private format(message: string, args: any[] = []): string { - let result: string; - if (args.length === 0) { - result = message; - } else { - result = message.replace(/\{(\d+)\}/g, (match, rest: any[]) => { - const index = rest[0]; - return typeof args[index] !== "undefined" ? args[index] : match; - }); - } - return result; - } - /** - * Get language pack - */ - private resolveLanguagePack(): ILanguagePack { - let resolvedLanguage: string = ""; - const rootPath = extensions.getExtension("yzhang.markdown-all-in-one").extensionPath; - const file = path.join(rootPath, "package"); - const options = this.config; - - if (!options.locale) { - resolvedLanguage = ".nls.json"; - } else { - let locale: string | null = options.locale; - while (locale) { - const candidate = ".nls." + locale + ".json"; - if (fs.existsSync(file + candidate)) { - resolvedLanguage = candidate; - break; - } else { - const index = locale.lastIndexOf("-"); - if (index > 0) { - locale = locale.substring(0, index); - } else { - resolvedLanguage = ".nls.json"; - locale = null; - } - } - } - } - - const languageFilePath = path.join(file + resolvedLanguage); - - if (!fs.existsSync(languageFilePath)) { - return {}; - } - - return JSON.parse(fs.readFileSync(languageFilePath, "utf-8")); - } -} - -let config: IConfig = { - locale: "en" -}; - -try { - config = Object.assign( - config, - JSON.parse((process.env as any).VSCODE_NLS_CONFIG) - ); -} catch (err) { - // -} - -const instance = new Localize(config); - -export default instance.localize.bind(instance); diff --git a/src/nls/README.md b/src/nls/README.md new file mode 100644 index 00000000..b50d421f --- /dev/null +++ b/src/nls/README.md @@ -0,0 +1,3 @@ +# National Language Support (NLS) + +This module follows a pattern similar to [vscode-nls](https://github.com/microsoft/vscode-nls). diff --git a/src/nls/index.ts b/src/nls/index.ts new file mode 100644 index 00000000..74393368 --- /dev/null +++ b/src/nls/index.ts @@ -0,0 +1,161 @@ +"use strict"; + +import * as fs from "fs"; +import * as vscode from "vscode"; +import VisualStudioCodeLocaleId from "../contract/VisualStudioCodeLocaleId"; +import resolveResource from "./resolveResource"; + +export type Primitive = string | number | bigint | boolean | symbol | undefined | null; + +export interface IConfigOption { + extensionContext: vscode.ExtensionContext; + locale?: VisualStudioCodeLocaleId; +} + +export interface IFuncLocalize { + + /** + * @param key The key of the format string of the message in the bundle. + * @param args An array of objects to format. + */ + (key: string, ...args: Primitive[]): string; +} + +interface IInternalOption { + + /** + * Indicates whether the extension is **not** running in development mode. + * The same as `ExtensionContext.extensionMode !== Development`. + */ + cacheResolution: boolean; + + /** + * The same as `ExtensionContext.extensionPath`. + */ + extensionPath: string; + + /** + * The default locale. + * This is internally treated as an arbitrary string. + */ + locale: VisualStudioCodeLocaleId | undefined; +} + +interface INlsBundle { + [key: string]: string; +} + +// https://github.com/microsoft/vscode-nls/blob/9fd18e6777276ebeb68ddf314ec2459abc6e3f4f/src/node/main.ts#L36-L46 +// https://github.com/microsoft/vscode/blob/dad5d39eb0a251a726388f547e8dc85cd96a184d/src/vs/base/node/languagePacks.d.ts +interface IVscodeNlsConfig { + locale: string; + availableLanguages: { + [pack: string]: string; + }; +} + +//#region Utility + +function readJsonFile(path: string): T { + return JSON.parse(fs.readFileSync(path, "utf8")); +} + +//#endregion Utility + +//#region Private + +// Why `Object.create(null)`: +// Once constructed, this is used as a readonly dictionary (map). +// It is performance-sensitive, and should not be affected by the outside. +// Besides, `Object.prototype` might collide with our keys. +const resolvedBundle: INlsBundle = Object.create(null); + +/** + * Internal options. + * Will be initialized in `config()`. + */ +const options: IInternalOption = Object.create(null); + +/** + * Updates the in-memory NLS bundle. + * @param locales An array of locale IDs. The default locale will be appended. + */ +function cacheBundle(locales: VisualStudioCodeLocaleId[] = []): void { + if (options.locale) { + locales.push(options.locale); // Fallback. + } + + // * We always provide `package.nls.json`. + // * Reverse the return value, so that we can build a bundle with nice fallback by a simple loop. + const files = resolveResource(options.extensionPath, "package.nls", "json", locales)!.reverse() as readonly string[]; + for (const path of files) { + try { + Object.assign(resolvedBundle, readJsonFile(path)); + } catch (error) { + console.error(error); // Log, and ignore the bundle. + } + } +} + +/** + * @param message A composite format string. + * @param args An array of objects to format. + */ +function format(message: string, ...args: Primitive[]): string { + if (args.length === 0) { + return message; + } else { + return message.replace(/\{(0|[1-9]\d*?)\}/g, (match: string, index: string): string => { + // `index` is zero-based. + return args.length > +index ? String(args[+index]) : match; + }); + } +} + +//#endregion Private + +//#region Public + +export const localize: IFuncLocalize = function (key: string, ...args: Primitive[]): string { + if (options.cacheResolution) { + const msg: string | undefined = resolvedBundle[key]; + return msg === undefined ? "[" + key + "]" : format(msg, ...args); + } else { + // When in development mode, hot reload, and reveal the key. + cacheBundle(); + const msg: string | undefined = resolvedBundle[key]; + return msg === undefined ? "[" + key + "]" : "[" + key.substring(key.lastIndexOf(".") + 1) + "] " + format(msg, ...args); + } +}; + +/** + * Configures the NLS module. + * + * You should only call it **once** in the application entry point. + */ +export function config(opts: IConfigOption) { + if (opts.locale) { + options.locale = opts.locale; + } else { + try { + const vscodeOptions = JSON.parse(process.env.VSCODE_NLS_CONFIG as string) as IVscodeNlsConfig; + options.locale = vscodeOptions.locale as any; + } catch (error) { + // Log, but do nothing else, in case VS Code suddenly changes their mind, or we are not in VS Code. + console.error(error); + } + } + + options.extensionPath = opts.extensionContext.extensionPath; + options.cacheResolution = opts.extensionContext.extensionMode !== vscode.ExtensionMode.Development; + + // Load and freeze the cache when not in development mode. + if (options.cacheResolution) { + cacheBundle(); + Object.freeze(resolvedBundle); + } + + return localize; +} + +//#endregion Public diff --git a/src/nls/resolveResource.ts b/src/nls/resolveResource.ts new file mode 100644 index 00000000..596c9ae4 --- /dev/null +++ b/src/nls/resolveResource.ts @@ -0,0 +1,94 @@ +"use strict"; + +import * as fs from "fs"; +import * as path from "path"; +import type VisualStudioCodeLocaleId from "../contract/VisualStudioCodeLocaleId"; + +/** + * Finds localized resources that match the given pattern under the directory. + * + * ### Remarks + * + * Comparison is case-**sensitive** (SameValueZero). + * + * When an exact match cannot be found, this function performs fallback as per RFC 4647 Lookup. + * + * Make sure the directory does not change. + * Call this function as **few** as possible. + * This function may scan the directory thoroughly, thus is very **expensive**. + * + * ### Exceptions + * + * * The path to the directory is not absolute. + * * The directory does not exist. + * * Read permission is not granted. + * + * @param directory The **absolute** file system path to the directory that Node.js can recognizes, including UNC on Windows. + * @param baseName The string that the file name begins with. + * @param suffix The string that the file name ends with. + * @param locales The locale IDs that can be inserted between `baseName` and `suffix`. Sorted by priority, from high to low. + * @param separator The string to use when joining `baseName`, `locale`, `suffix` together. Defaults to `.` (U+002E). + * @returns An array of absolute paths to matched files, sorted by priority, from high to low. Or `undefined` when no match. + * @example + * // Entries under directory `/tmp`: + * // Directory f.nls.zh-cn.json/ + * // File f.nls.json + * // File f.nls.zh.json + * + * resolveResource("/tmp", "f.nls", "json", ["ja", "zh-cn"]); + * + * // Returns: + * ["/tmp/f.nls.zh.json", "/tmp/f.nls.json"]; + */ +export default function resolveResource( + directory: string, + baseName: string, + suffix: string, + locales: VisualStudioCodeLocaleId[], + separator: string = ".", +): string[] | undefined { + if (!path.isAbsolute(directory)) { + throw new Error("The directory must be an absolute file system path."); + } + + // Throw an exception, if we do not have permission, or the directory does not exist. + const files: readonly string[] = fs.readdirSync(directory, { withFileTypes: true }).reduce((res, crt) => { + if (crt.isFile()) { + res.push(crt.name); + } + return res; + }, []); + + const result: string[] = []; + + let splitIndex: number; + for (let loc of locales as string[]) { + while (true) { + const fileName = baseName + separator + loc + separator + suffix; + const resolvedPath = path.resolve(directory, fileName); + if (!result.includes(resolvedPath) && files.includes(fileName)) { + result.push(resolvedPath); + } + + // Fallback according to RFC 4647 section 3.4. Although they are different systems, algorithms are common. + splitIndex = loc.lastIndexOf("-"); + if (splitIndex > 0) { + loc = loc.slice(0, splitIndex); + } else { + break; + } + } + } + + // Fallback. The use of block is to keep the function scope clean. + { + const fileName = baseName + separator + suffix; + const resolvedPath = path.resolve(directory, fileName); + // As long as parameters are legal, this `resolvedPath` won't have been in `result`. Thus, only test `fileName`. + if (files.includes(fileName)) { + result.push(resolvedPath); + } + } + + return result.length === 0 ? undefined : result; +} diff --git a/src/print.ts b/src/print.ts index c9a45d1d..e0acf5d0 100644 --- a/src/print.ts +++ b/src/print.ts @@ -4,7 +4,7 @@ import * as fs from "fs"; import * as path from 'path'; import { commands, ExtensionContext, TextDocument, Uri, window, workspace } from 'vscode'; import { encodeHTML } from 'entities'; -import localize from './localize'; +import { localize } from './nls'; import { mdEngine, extensionBlacklist } from "./markdownEngine"; import { isMdEditor } from './util'; @@ -34,7 +34,7 @@ async function print(type: string, uri?: Uri, outFolder?: string) { const editor = window.activeTextEditor; if (!isMdEditor(editor)) { - window.showErrorMessage(localize("noValidMarkdownFile")); + window.showErrorMessage(localize("ui.general.messageNoValidMarkdownFile")); return; } @@ -43,7 +43,7 @@ async function print(type: string, uri?: Uri, outFolder?: string) { doc.save(); } - window.setStatusBarMessage(localize("printing") + ` '${path.basename(doc.fileName)}' ` + localize("to") + ` '${type.toUpperCase()}' ...`, 1000); + const statusBarMessage = window.setStatusBarMessage("$(sync~spin) " + localize("ui.exporting.messageExportingInProgress", path.basename(doc.fileName), type.toUpperCase())); if (outFolder && !fs.existsSync(outFolder)) { fs.mkdirSync(outFolder, { recursive: true }); @@ -109,7 +109,7 @@ async function print(type: string, uri?: Uri, outFolder?: string) { const file = fs.readFileSync(imgSrc.replace(/%20/g, '\ ')).toString('base64'); return `${p1}data:image/${imgExt};base64,${file}${p3}`; } catch (e) { - window.showWarningMessage(`${localize("unableToReadFile")} '${imgSrc}'. ${localize("revertingToImagePaths")}. (${doc.fileName})`); + window.showWarningMessage(localize("ui.general.messageUnableToReadFile", imgSrc) + ` ${localize("ui.exporting.messageRevertingToImagePaths")} (${doc.fileName})`); } if (configAbsPath) { @@ -168,6 +168,9 @@ async function print(type: string, uri?: Uri, outFolder?: string) { case 'pdf': break; } + + // Hold the message for extra 500ms, in case the operation finished very fast. + setTimeout(() => statusBarMessage.dispose(), 500); } function batchPrint() { @@ -217,13 +220,10 @@ function wrapWithStyleTag(src: string) { function readCss(fileName: string) { try { - return fs.readFileSync(fileName).toString().replace(/\s+/g, ' '); + return fs.readFileSync(fileName).toString(); } catch (error) { - let msg = error.message.replace('ENOENT: no such file or directory, open', localize("customStyle")) + localize("notFound"); - msg = msg.replace(/'([c-z]):/, function (_, g1) { - return `'${g1.toUpperCase()}:`; - }); - window.showWarningMessage(msg); + // https://nodejs.org/docs/latest-v12.x/api/errors.html#errors_class_systemerror + window.showWarningMessage(localize("ui.exporting.messageCustomCssNotFound", (error as NodeJS.ErrnoException).path)); return ''; } } diff --git a/src/util.ts b/src/util.ts index 1e0699a7..153a3602 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,7 +1,6 @@ 'use strict'; import { commands, DocumentSelector, Position, Range, TextDocument, TextEditor, Uri, workspace } from 'vscode'; -import localize from './localize'; import { commonmarkEngine, mdEngine } from './markdownEngine'; import { decodeHTML } from 'entities'; import LanguageIdentifier from "./contract/LanguageIdentifier"; @@ -97,32 +96,6 @@ export function isFileTooLarge(document: TextDocument): boolean { } } -/* ┌───────────┐ - │ Changelog │ - └───────────┘ */ - -export function getNewFeatureMsg(version: string) { - switch (version) { - case '1.3.0': - return localize("1.3.0 msg"); - case '1.4.0': - return localize("1.4.0 msg"); - case '1.5.0': - return localize("1.5.0 msg"); - case '2.1.0': - return localize("2.1.0 msg"); - case '2.4.0': - return localize("2.4.0 msg"); - case '3.0.0': - return localize("3.0.0 msg"); - } - return undefined; -} - -export function showChangelog() { - commands.executeCommand('vscode.open', Uri.parse('https://github.com/yzhang-gh/vscode-markdown/blob/master/CHANGELOG.md')); -} - /* ┌─────────────────┐ │ Text Extraction │ └─────────────────┘ */ diff --git a/tools/prepublish.js b/tools/prepublish.js new file mode 100644 index 00000000..5f3a50db --- /dev/null +++ b/tools/prepublish.js @@ -0,0 +1,63 @@ +/*! + * Licensed under the MIT License. + */ + +// This is designed for vsce pre-publish. See `package.json`. +// https://code.visualstudio.com/api/working-with-extensions/publishing-extension#prepublish-step + +//@ts-check + +"use strict"; + +const { spawn } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +const projectRootPath = path.resolve(__dirname, ".."); + +/* Check environment. */ + +// Warn if the caller might be using this module in a wrong way. +if (process.cwd() !== projectRootPath) { + console.error(`\nYour working directory is: ${process.cwd()}\nNot the project root.\nAre you calling it in a correct way?\n`); +} + +/* Prepare welcome materials. */ + +console.log("\nPrepare welcome materials...\n"); + +const welcomeDirPath = path.resolve(projectRootPath, "welcome"); + +// Delete `WELCOMED` flag file. +try { + fs.unlinkSync(path.resolve(welcomeDirPath, "WELCOMED")); +} catch { } + +// vsce will modify `README.md` and `CHANGELOG.md` during packaging. +// Thus, we create the `changes.md` for our extension to consume. +// Due to relative paths in the file, it has to be under the project root. +let isWelcomeMessagesExist = false; +try { + isWelcomeMessagesExist = fs.readdirSync(welcomeDirPath, { withFileTypes: true }) + .some(i => i.isFile() && i.name.endsWith(".txt")); +} catch { } + +if (isWelcomeMessagesExist) { + const srcPath = path.resolve(projectRootPath, "CHANGELOG.md"); + const destPath = path.resolve(projectRootPath, "changes.md"); + + fs.copyFileSync(srcPath, destPath); + console.log(`Copied.\nFrom: ${srcPath}\nTo: ${destPath}`); +} else { + console.error("Skipped: Create 'changes.md'."); +} + +/* Compile. */ + +console.log("\nCompile extension...\n"); + +spawn("npx", ["webpack", "--mode", "production"], { + cwd: projectRootPath, + shell: process.platform === "win32", // Windows compatibility. + stdio: "inherit", +}).on("error", e => { throw e; }); diff --git a/tools/set-welcome-message.js b/tools/set-welcome-message.js new file mode 100644 index 00000000..6cba55c9 --- /dev/null +++ b/tools/set-welcome-message.js @@ -0,0 +1,59 @@ +/*! + * Licensed under the MIT License. + */ + +// This helps you create a welcome message file. +// It can be either imported as a function, or run as a script. +// As a script, it requires one or two arguments: the welcome message, the locale ID. +// If anything goes wrong, an exception will be thrown. + +//@ts-check + +"use strict"; + +const fs = require("fs"); +const path = require("path"); + +/** + * Creates or overwrites a welcome message file (`welcome/.txt`) in the project root. + * + * Due to security concerns, code points under Unicode General Category C are not allowed. + * Thus, EOLs are also not allowed in both the file and the parameter. + * Thus, the definition of "line" here differs greatly from the POSIX standard. + * @param {string} message The welcome message. Must be single line, and safe for display. + * @param {string} locale The locale ID, which should be recognized by VS Code. + * But you can still provide an arbitrary one, as long as it matches the format. + * @returns `true` for success. + */ +function setWelcomeMessage(message, locale = "en") { + if (!message || /^\s*$/.test(message)) { + throw new Error("The message must contain non-whitespace characters."); + } + + if (/\p{C}/u.test(message)) { + throw new Error("Control characters and other code points under Unicode General Category C are not allowed."); + } + + if (!/^[A-Za-z]+(?:-[A-Za-z]+)*$/.test(locale)) { + throw new Error("The locale ID must only contain ASCII letters or hyphens, and must begin and end with letters."); + } + + const messageFilePath = path.resolve(__dirname, "..", "welcome", locale + ".txt"); + + fs.mkdirSync(path.resolve(__dirname, "..", "welcome"), { recursive: true }); + fs.writeFileSync(messageFilePath, message, "utf8"); + console.log(`\nSucceeded.\nMessage: ${message}\nPath: ${messageFilePath}\n`); + return true; +} + +module.exports = setWelcomeMessage; + +/* Main. */ + +if (process.argv[1] === __filename) { + if (process.argv.length <= 4) { + setWelcomeMessage(process.argv[2], process.argv[3]); + } else { + throw new Error("\nThis requires one or two arguments.\nAre you calling it in a correct way?\n"); + } +}