diff --git a/.gitignore b/.gitignore index 3c4a5fa..2c0f374 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ settings.json # ignore dist/ dist +log release.zip \ No newline at end of file diff --git a/docs/debugging.md b/docs/debugging.md new file mode 100644 index 0000000..da523bf --- /dev/null +++ b/docs/debugging.md @@ -0,0 +1,45 @@ +部分情况下,您可能需要获取本插件的调试信息。 + +# 确保已开启调试输出 + +首先,请确保您已经开启本插件的调试信息输出功能,包括 `控制台输出` 以及 `日志文件输出` 。 + +![image](https://github.com/d0j1a1701/LiteLoaderQQNT-Markdown/assets/61616918/ca06d6e6-60b7-45c2-85bc-111b69bf47be) + +## 开启元素抓取 + +部分情况下,开发者可能需要相关的元素调试信息来定位问题,您可以开启本插件设置中的 `启用元素调试` 功能。 + +![image](https://github.com/d0j1a1701/LiteLoaderQQNT-Markdown/assets/61616918/1bf4e4d5-e3f4-4190-9c10-4b6153103fb7) + + +启用该功能后,当您发送的消息包含:\`--mdit-debug-capture-element\` 时(需包含\`符号,该标志将会被渲染为`code`元素),该消息会被作为调试用消息进行抓取,并将相关信息保存到日志文件中。**在本功能启用时,请确保您发送的调试消息不包含任何您不想让其他人看到的敏感信息。** 请看下例: + +- 消息框中输入的消息 + +![image](https://github.com/d0j1a1701/LiteLoaderQQNT-Markdown/assets/61616918/db4b29ee-1ec3-4c76-85ce-d5c2639ef254) + +- 消息渲染结果 + +![image](https://github.com/d0j1a1701/LiteLoaderQQNT-Markdown/assets/61616918/4dab43b9-6988-4f55-be0a-e84514e2e8f8) + +为保证隐私性,只有自己发送的消息才能被标记为调试消息。 + + +# 进入日志文件目录 + +首先进入 `LiteLoaderQQNT` 插件设置页面,进入数据目录。 + +![image](https://github.com/d0j1a1701/LiteLoaderQQNT-Markdown/assets/61616918/bbbae1b8-fdd1-4daa-848f-1ec654dc9407) + +随后依次进入 `plugins -> markdown_it -> log`,便能看到所有日志文件。 + +> 如果目录不存在,一般是由于之前从未开启过日志文件输出功能。在完成上一步开启调试输出后,重启QQ即可。 + +# 提供文件给开发者 + +您可以直接将文件发送给相关的开发者,也可以选择用文本编辑工具打开 `.log` 文件并复制其内容进行分享。如果目录内存在多个文件,一般情况下请提供最新的文件。 + +# 清理日志文件 + +您可以在 `QQNT` 未启动的情况下,直接删除日志文件中的所有日志。 \ No newline at end of file diff --git a/docs/dev/renderer.md b/docs/dev/renderer.md index 2f6c594..aa0d8c6 100644 --- a/docs/dev/renderer.md +++ b/docs/dev/renderer.md @@ -5,8 +5,9 @@ - [Introduction](#introduction) - [Start Developing](#start-developing) + - [Logging](#logging) - [Create Release Version](#create-release-version) -- [UI Development](#ui-development) + - [UI Development](#ui-development) - [Content Rendering Test Example](#content-rendering-test-example) @@ -30,6 +31,21 @@ npm run dev This will start `webpack` in watch mode with `development` flag enabled. +## Logging + +A custom `conole` wrapper `mditLogger` is recommend when you need to log something in *Renderer Process*. You can import the logger by: + +```javascript +import { mditLogger } from './utils/logger'; + +function someFunc() { + mditLogger('debug', 'debug info here.'); // Output: [MarkdownIt] debug info here. +} +``` + + + +Use `mditLogger` whenever possible. ## Create Release Version @@ -45,7 +61,7 @@ The script `npm run release` will: 2. Run `git archive` to create a `release.zip` file contains all code included in `git`. 3. Run `zip -r` to add `dist` directory into previously generated `release.zip`. -# UI Development +## UI Development You could use `React` to develop plugin settings UI interface. The entrance of user settings page is `src/components/setting_page.js` diff --git a/manifest.json b/manifest.json index e775daf..6f22d4a 100644 --- a/manifest.json +++ b/manifest.json @@ -27,7 +27,7 @@ ], "injects": { "renderer": "./dist/renderer.js", - "main": "./src/main.js", + "main": "./dist/main.js", "preload": "./src/preload.js" } } \ No newline at end of file diff --git a/src/components/setting_page.jsx b/src/components/setting_page.jsx index c9795f7..99f37c4 100644 --- a/src/components/setting_page.jsx +++ b/src/components/setting_page.jsx @@ -1,6 +1,7 @@ import React from 'react'; import { useSettingsStore } from '@/states/settings'; +import { mditLogger } from '@/utils/logger'; export function SettingPage() { @@ -9,15 +10,17 @@ export function SettingPage() { // use encapsulated and whenever possible. return (<> - - - 设置更新 - 在此页面更新设置后,需要重启QQ后方可生效 - + + + + + + + @@ -77,12 +80,40 @@ export function SettingPage() { + + + + + + + + + + + + + + + + + - { updateSetting(settingName, !settings[settingName]) }} is-active={settingsValue == true ? true : undefined} is-disabled={getForceValue()} + style={{ + 'flex': 'none', + }} > ); @@ -145,7 +178,7 @@ function TextAndCaptionBlock({ title, caption }) { 'display': 'flex', 'flexWrap': 'wrap', 'flexDirection': 'column', - 'width': '92%', + 'flex': '1 1 auto', }}> {title} {caption}

); +} + +function ButtonTile({ title, caption, href, path, callback, actionName }) { + callback ??= function () { + if (href !== undefined && href !== null) { + LiteLoader.api.openExternal(href); + return; + } + if (path !== undefined && path !== null) { + LiteLoader.api.openPath(path); + return; + } + mditLogger('debug', 'Button with no action clicked'); + + } + + return ( + + + + {actionName} + + + ); } \ No newline at end of file diff --git a/src/main.js b/src/main.js index cf40889..3a2faec 100644 --- a/src/main.js +++ b/src/main.js @@ -1,58 +1,26 @@ // 运行在 Electron 主进程 下的插件入口 -const { shell, ipcMain } = require("electron"); -const fs = require('fs/promises'); +const { ipcMain } = require("electron"); +import { generateMainProcessLogerWriter, LogPathHelper } from '@/utils/logger_main'; -const plugin_path = LiteLoader.plugins["markdown_it"].path.plugin; +const loggerWriter = generateMainProcessLogerWriter(); -// import { shell, ipcMain } from "electron"; - -const setttingsJsonFilePath = `${plugin_path}/settings.json`; - -async function getSettings(key) { - var json = JSON.parse(await fs.readFile(setttingsJsonFilePath)); - return json[key]; -} - -async function updateSettings({ name, value }) { - var json = {}; - try { json = JSON.parse(await fs.readFile(setttingsJsonFilePath)); } catch (e) { - ; +function onBrowserWindowCreated() { + try { onLoad(); } catch (e) { + console.error('[markdown-it]', e); } - json[name] = value; - await fs.writeFile(setttingsJsonFilePath, JSON.stringify(json)); -} - -async function removeSettings(key) { - var json = JSON.parse(await fs.readFile(setttingsJsonFilePath)); - json[key] = undefined; - await fs.writeFile(setttingsJsonFilePath, JSON.stringify(json)); } -onLoad(); - // 加载插件时触发 function onLoad() { - ipcMain.handle("LiteLoader.markdown_it.open_link", (event, content) => { - if (content.indexOf("http") != 0) { - content = "http://" + content; - } - return shell.openExternal(content); - }); - - ipcMain.handle('LiteLoader.markdown_it.get_settings', (e, key) => { - return getSettings(key); - }); - ipcMain.handle('LiteLoader.markdown_it.update_settings', (e, { name, value }) => { - return updateSettings({ name, value }); + ipcMain.handle('LiteLoader.markdown_it.log', (e, consoleMode, ...args) => { + loggerWriter(consoleMode, ...args); }); - ipcMain.handle('LiteLoader.markdown_it.remove_settings', (e, key) => { - return removeSettings(key); - }) + ipcMain.handle('LiteLoader.markdown_it.get_log_path', (e) => LogPathHelper.getLogFolderPath()); } // 这两个函数都是可选的 -module.exports = { - onLoad, +export { + onBrowserWindowCreated, }; diff --git a/src/preload.js b/src/preload.js index 2286ca7..37a1fe8 100644 --- a/src/preload.js +++ b/src/preload.js @@ -3,14 +3,16 @@ const { contextBridge, ipcRenderer } = require("electron"); // 在window对象下导出只读对象 contextBridge.exposeInMainWorld("markdown_it", { - render: (content) => - ipcRenderer.invoke("LiteLoader.markdown_it.render", content), - open_link: (content) => - ipcRenderer.invoke("LiteLoader.markdown_it.open_link", content), - get_settings: (key) => - ipcRenderer.invoke("LiteLoader.markdown_it.get_settings", key), - update_settings: ({ name, value }) => - ipcRenderer.invoke("LiteLoader.markdown_it.update_settings", { name, value }), - remove_settings: (key) => - ipcRenderer.invoke("LiteLoader.markdown_it.remove_settings", key), + // render: (content) => + // ipcRenderer.invoke("LiteLoader.markdown_it.render", content), + // open_link: (content) => + // ipcRenderer.invoke("LiteLoader.markdown_it.open_link", content), + // get_settings: (key) => + // ipcRenderer.invoke("LiteLoader.markdown_it.get_settings", key), + // update_settings: ({ name, value }) => + // ipcRenderer.invoke("LiteLoader.markdown_it.update_settings", { name, value }), + // remove_settings: (key) => + // ipcRenderer.invoke("LiteLoader.markdown_it.remove_settings", key), + log: (consoleType, ...args) => ipcRenderer.invoke('LiteLoader.markdown_it.log', consoleType, ...args), + get_log_path: () => ipcRenderer.invoke('LiteLoader.markdown_it.get_log_path'), }); diff --git a/src/renderer.jsx b/src/renderer.jsx index 580ca1d..ddb7fe7 100644 --- a/src/renderer.jsx +++ b/src/renderer.jsx @@ -25,7 +25,7 @@ import { useSettingsStore } from '@/states/settings'; // Utils import { debounce } from 'throttle-debounce'; -import { mditLogger } from './utils/logger'; +import { mditLogger, elementDebugLogger } from './utils/logger'; const markdownRenderedClassName = 'markdown-rendered'; @@ -203,10 +203,11 @@ function render() { }) messageBox.removeChild(posBase); - changeDirectionToColumnWhenLargerHeight(); - - }) + }); + // code that runs after renderer work finished. + changeDirectionToColumnWhenLargerHeight(); + elementDebugLogger(); } function loadCSSFromURL(url, id) { diff --git a/src/states/settings.ts b/src/states/settings.ts index 52ea52d..016751b 100644 --- a/src/states/settings.ts +++ b/src/states/settings.ts @@ -20,6 +20,8 @@ export interface SettingStateProperties { // Debug settings consoleOutput: boolean; // If false, mditLogger will not output to console. + fileOutput: boolean; // If false, mditLogger will not add log into log file. + enableElementCapture: boolean; } export interface SettingStateAction { @@ -59,6 +61,8 @@ export const useSettingsStore = create { if (get().unescapeAllHtmlEntites === true) { diff --git a/src/utils/liteloader_type.ts b/src/utils/liteloader_type.ts index 03c3527..6cc9379 100644 --- a/src/utils/liteloader_type.ts +++ b/src/utils/liteloader_type.ts @@ -3,13 +3,50 @@ * * Notice the `new_config` and `default_config` should all be a Object. Passing string will cause unexpected behaviour. */ -export interface LiteLoaderInterFace { + +export interface LiteLoaderInterFace { + path: { + root: string; // 本体目录路径 + profile: string; // 存储目录路径(如果指定了 LITELOADERQQNT_PROFILE 环境变量) + data: string; // 数据目录路径 + plugins: string; // 插件目录路径 + }; + versions: { + qqnt: string; // QQNT 版本号 + liteloader: string; // LiteLoaderQQNT 版本号 + node: string; // Node.js 版本号 + chrome: string; // Chrome 版本号 + electron: string; // Electron 版本号 + }; + os: { + platform: string; // 系统平台名称 + }; + package: { + liteloader: object; // LiteLoaderQQNT package.json 文件内容 + qqnt: object; // QQNT package.json 文件内容 + }; + plugins: { + markdown_it: { + incompatible: boolean; // 插件是否兼容 + disabled: boolean; // 插件是否禁用 + manifest: object; // 插件 manifest.json 文件内容 + path: { + plugin: string; // 插件本体根目录路径 + data: string; // 插件数据根目录路径 + injects: { + main: string; // 插件主进程脚本文件路径 + renderer: string; // 插件渲染进程脚本文件路径 + preload: string; // 插件预加载脚本文件路径 + }; + }; + }; + }; api: { - openPath(path: string): any; - openExternal(uri: string): any; + openPath(path: string): void; // 打开指定目录 + openExternal(uri: string): void; // 打开外部连接 config: { - set(slug: string, new_config: T): Promise; - get(slug: string, default_config: T): Promise; - } - }, -}; \ No newline at end of file + set(slug: string, newConfig: ConfigInfoType): Promise; // 设置配置文件 + get(slug: string, defaultConfig: ConfigInfoType): Promise; // 获取配置文件 + }; + }; +} diff --git a/src/utils/logger.ts b/src/utils/logger.ts index c49c477..a7952e6 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -4,6 +4,10 @@ import { useSettingsStore } from '@/states/settings'; +declare const markdown_it: { + log: (consoleType: string, ...args: any[]) => any +}; + type DistributiveFilter = Origin extends Filter ? Origin : never; /** @@ -15,29 +19,100 @@ type LoggerFuncKey = { export type SupportedLoggerFuncKey = DistributiveFilter; -function showOuputToConsole() { +function outputToConsoleSettingEnabled() { return useSettingsStore.getState().consoleOutput; } +function outputToFileSettingEnabled() { + return useSettingsStore.getState().fileOutput; +} + +export interface MditLoggerOptions { + /** + * Determine if a log will be output in DevTools console + */ + consoleOutput: boolean; + /** + * Determine if a log will be persisted in file + */ + fileOutput: boolean; +} + +const defaultMditLoggerOptions: MditLoggerOptions = { + consoleOutput: true, + fileOutput: true, +} + +export function mditLoggerGenerator(options: MditLoggerOptions = defaultMditLoggerOptions): + (consoleFunction: SupportedLoggerFuncKey, ...params: any[]) => (undefined) { + return function (consoleFunction: SupportedLoggerFuncKey, ...params: any[]) { + if (outputToConsoleSettingEnabled() && options.consoleOutput) { + console[consoleFunction]( + '%c [MarkdownIt] ', + 'background-color: rgba(0, 149, 204, 0.8); border-radius: 6px;padding-block: 2px; padding-inline: 0px; color: white;', + ...params); + } + + if (outputToFileSettingEnabled() && options.fileOutput) { + markdown_it.log(consoleFunction, ...params); + } + + return undefined; + } +} + /** * Markdown it console output wrapper. * - * @param consoleFunction The function key of `console`. For exmaple: `'debug'` + * @param consoleFunction The name of the member function you want to use in `console`. For exmaple: `'debug'` or `'info'` * @param params Params that passed to `console` function. * * @example * * ```js - * mditLogger('debug', 'This is a debug message'); + * mditLogger('debug', 'This is a debug message', {name: 'Jobs', age: 17}); * ``` */ -export function mditLogger(consoleFunction: SupportedLoggerFuncKey, ...params: any[]) { - if (!showOuputToConsole()) { - return undefined; +export const mditLogger = mditLoggerGenerator(); + + +const loggedClassName = '--mdit-debug-capture-element-logged'; +const logFlagClassName = '--mdit-debug-capture-element'; + +export function elementDebugLogger() { + var enabled = useSettingsStore.getState().enableElementCapture; + if (!enabled) { + return; } - return console[consoleFunction]( - '%c [MarkdownIt] ', - 'background-color: rgba(0, 149, 204, 0.8); border-radius: 6px;padding-block: 2px; padding-inline: 0px; color: white;', - ...params); -} + mditLogger('info', 'ElementCapture triggered'); + var codeEle = document.querySelectorAll('div.message-content__wrapper div.container--self code'); + + // Add flag class for all marked --mdit-debug-capture-element + Array.from(codeEle) + .filter((ele) => ele.innerHTML == logFlagClassName) + .forEach((ele) => { + ele.classList.add(logFlagClassName); + }); + + // find all self sent message box that has been marked to capture, then log it. + var flaggedMsgBoxs = document.querySelectorAll(`div.message-content__wrapper div.container--self:has(.${logFlagClassName})`); + + var loggedCount = 0; + Array + .from(flaggedMsgBoxs) + .filter((ele) => !ele.classList.contains(loggedClassName)) // ensure one message box will only be logged one time + .forEach((ele) => { + // file-only logger + mditLoggerGenerator({ + ...defaultMditLoggerOptions, + consoleOutput: false, + })('log', ele.outerHTML); + + mditLogger('debug', `Element captured: ${ele.tagName}`); + + ele.classList.add(loggedClassName); + loggedCount++; + }); + mditLogger('info', 'Element Capture Finished:', `${loggedCount} element(s) has been logged`); +} \ No newline at end of file diff --git a/src/utils/logger_main.ts b/src/utils/logger_main.ts new file mode 100644 index 0000000..c63000e --- /dev/null +++ b/src/utils/logger_main.ts @@ -0,0 +1,63 @@ +import * as path from 'path'; +import { existsSync, mkdirSync, createWriteStream } from 'fs'; +import { writeFile } from 'fs/promises'; +import { LiteLoaderInterFace } from '@/utils/liteloader_type'; + +declare const LiteLoader: LiteLoaderInterFace; + +export const LogPathHelper = { + getLogFolderPath() { + return path.join(LiteLoader.plugins.markdown_it.path.plugin, 'log'); + }, + + /** + * Get absolute file path of a log file. + * + * @param logFileName Name of the log file. If `undefined`, + * will generate automatically based on current time. + */ + getLogFilePath(logFileName?: string | undefined) { + // generate log file name if not received + logFileName ??= (new Date().toISOString()).replaceAll(':', '-'); + + return path.join(LiteLoader.plugins.markdown_it.path.plugin, 'log', `${logFileName}.log`); + } +}; + +/** + * Generate a writer function that used to write log into log file. + */ +export function generateMainProcessLogerWriter() { + var logFolderPath = LogPathHelper.getLogFolderPath(); + var logFilePath = LogPathHelper.getLogFilePath(); + + console.log(`[markdown-it] logFolderPath: ${logFolderPath}`); + console.log(`[markdown-it] logFilePath: ${logFilePath}`); + + // create dir if not exists + try { + if (!existsSync(logFolderPath)) { + mkdirSync(logFolderPath, { recursive: true }); + } + } catch (err) { + console.error(err); + } + + var stream = createWriteStream(logFilePath, { + flags: 'a+', + }) + + return async function (consoleMode: string, ...args: any[]) { + var timeStr = new Date().toISOString(); + + var argsStr = args.reduce(function (str, value) { + if (typeof value === 'string') { + return str + value; + } + return str + JSON.stringify(value); + }, ''); + + var logStr = `${consoleMode.toUpperCase()} | ${timeStr} | ${argsStr}`; + stream.write(`${logStr}\n`); + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 85dcb2c..c88b6c2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,9 @@ "noImplicitAny": true, "jsx": "react", "allowJs": true, + "target": "ES2021", + "module": "NodeNext", + "moduleResolution": "NodeNext" }, "exclude": [ "./dist/**/*" // This is what fixed it! diff --git a/webpack.common.js b/webpack.common.js index b45acfe..f1fe2fd 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -62,16 +62,16 @@ const mainProcessConfig = { // Explicitly resolve files with following extension as modules. extensions: ['', '.js', '.jsx', '.ts', '.tsx'], }, - experiments: { - outputModule: true, - }, + // experiments: { + // outputModule: true, + // }, target: 'electron-main', entry: "./src/main.js", output: { path: path.resolve(__dirname, "dist"), filename: 'main.js', library: { - type: 'commonjs', // necessary in order to work with liteloader. + type: 'commonjs-static', // necessary in order to work with liteloader. }, chunkFormat: 'module', // or 'module' },