diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..a5b32b2 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# 默认由仓库维护者审阅所有变更。 +* @Hughhhhcoder diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..6afef41 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,58 @@ +name: Bug 报告 +description: 报告一个可以稳定复现的问题 +title: "[Bug]: " +labels: + - bug +body: + - type: markdown + attributes: + value: | + 感谢反馈!提交前请先确认问题能在最新线上版本或 `master` 分支复现。 + + - type: input + id: browser + attributes: + label: 浏览器与版本 + placeholder: 例如 Chrome 128 / Safari 18 + validations: + required: true + + - type: input + id: tool + attributes: + label: 受影响的工具或页面 + placeholder: 例如 Markdown 编辑器、二维码工具 + validations: + required: true + + - type: textarea + id: steps + attributes: + label: 复现步骤 + description: 请按顺序写出最小复现路径。 + placeholder: | + 1. 打开…… + 2. 输入…… + 3. 点击…… + validations: + required: true + + - type: textarea + id: expected + attributes: + label: 预期行为 + validations: + required: true + + - type: textarea + id: actual + attributes: + label: 实际行为 + validations: + required: true + + - type: textarea + id: context + attributes: + label: 补充信息 + description: 可以附截图、控制台错误、操作系统或其他上下文。请先移除敏感数据。 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..e61d373 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: 在线体验 + url: https://htool.vercel.app/ + about: 先在最新线上版本确认问题是否可以复现。 + - name: 使用文档 + url: https://github.com/Hughhhhcoder/HTooL#快速开始 + about: 查看安装、测试、部署和隐私说明。 diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..21792b8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,34 @@ +name: 功能建议 +description: 提议一个能让 HTooL 更有用的改进 +title: "[Feature]: " +labels: + - enhancement +body: + - type: textarea + id: problem + attributes: + label: 要解决的问题 + description: 请描述场景和当前遇到的限制,而不仅是解决方案。 + placeholder: 我在……场景下,需要…… + validations: + required: true + + - type: textarea + id: proposal + attributes: + label: 建议方案 + description: 你希望工具如何工作? + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: 备选方案 + description: 你尝试过哪些替代做法? + + - type: textarea + id: context + attributes: + label: 补充信息 + description: 可以附示例、草图、截图或相关链接。 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..ab5ef39 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,27 @@ +## 变更摘要 + + + +## 关联 Issue + + + +## 验证方式 + +- [ ] `npm run test:unit` +- [ ] `npm run test:no-gradient` +- [ ] `npm run build` +- [ ] `npm run test:perf-budget` +- [ ] `npm run test:e2e` + +## 检查清单 + +- [ ] 我已检查浅色和暗色模式。 +- [ ] 我已检查移动端布局。 +- [ ] 我已补充或更新了相关测试。 +- [ ] 我没有提交密钥、个人数据或真实用户文件。 +- [ ] 如果涉及 UI,我已附上截图或录屏。 + +## 兼容性与风险 + + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ef16883 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..50a155a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +permissions: + contents: read + +jobs: + verify: + name: Verify + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run unit and component tests + run: npm run test:unit + + - name: Check design constraints + run: npm run test:no-gradient + + - name: Build production bundle + run: npm run build + + - name: Check performance budget + run: npm run test:perf-budget + + - name: Install Playwright Chromium + run: npx playwright install --with-deps chromium + + - name: Run end-to-end tests + run: npm run test:e2e diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..ab2cfa1 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,23 @@ +# 行为准则 + +## 我们的承诺 + +为了营造开放、友善且可持续的协作环境,参与 HTooL 的每个人都应以尊重、善意和专业的方式交流,无论其经验、背景或身份如何。 + +## 可接受的行为 + +- 使用清晰、建设性的语言提出意见; +- 讨论实现、体验和证据,而不是评价个人; +- 接受不同观点,并在有新信息时修正自己的判断; +- 尊重项目维护者和其他贡献者的时间。 + +## 不可接受的行为 + +- 骚扰、歧视、威胁、人身攻击或故意羞辱; +- 发布他人的私人信息; +- 破坏讨论、刷屏、冒充他人或以恶意方式提交内容; +- 任何不适合公开协作环境的行为。 + +## 执行 + +请通过 [维护者主页](https://github.com/Hughhhhcoder) 私下联系维护者,报告违反本准则的行为。维护者会根据事实采取适当措施,并尽可能保护报告者的隐私。 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d3237c7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,53 @@ +# 贡献指南 + +感谢你愿意改进 HTooL。小而明确的 Pull Request 更容易被审阅,也更容易保持这个工具箱的稳定和克制。 + +## 开始之前 + +1. 确认 Node.js 版本为 20 或更高版本。 +2. 安装依赖:`npm ci`。 +3. 启动开发环境:`npm run dev`。 +4. 对行为变更先搜索现有 Issue,避免重复工作。 + +## 开发约定 + +- 页面放在 `src/views/`,跨页面组件放在 `src/components/`,纯逻辑放在 `src/utils/`。 +- 新增功能时同步补充单元/组件测试;涉及用户流程时补充 E2E 测试。 +- 保持工具页面响应式,并同时检查浅色和暗色模式。 +- 项目当前有两个设计门禁:不要在 `src/` 中使用 CSS gradient,并确保构建产物不超过性能预算。 +- 不要把密钥、个人数据或真实用户文件提交到仓库。 + +## 分支与提交 + +从 `master` 创建分支,例如: + +```bash +git switch -c feat/your-change +``` + +提交信息建议使用简短的 Conventional Commits 风格前缀: + +```text +feat: add a new utility +fix: handle invalid input +docs: improve setup guide +refactor: simplify converter state +test: cover keyboard shortcut +``` + +## 提交 Pull Request + +提交前至少运行: + +```bash +npm run check +``` + +如果本地没有 Playwright 浏览器,先运行 `npx playwright install chromium`。PR 描述请说明: + +- 做了什么,以及为什么做; +- 如何验证; +- 是否有 UI 变化(有的话附截图或录屏); +- 是否存在兼容性、性能或破坏性变更。 + +PR 会经过 CI 检查。维护者可能会要求拆分范围、补测试或调整交互细节。 diff --git a/README.md b/README.md index c27e71c..ee88487 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,159 @@ -# HTooL +
+ HTooL 图标 +

HTooL

+

一套浏览器优先的实用工具箱,把常用的小工具收进一个克制、顺手的工作台。

+

+ 在线体验 + · + 报告问题 + · + 参与贡献 +

+
+ +

+ CI 状态 + GitHub Issues + 在线演示 +

+ +## 项目定位 + +HTooL 面向写作、开发和日常信息处理场景,优先把数据留在浏览器中完成处理。它不是一个需要账号和后端服务的复杂平台,而是一组打开即用、按任务分组的轻量工具。 + +## 工具一览 + +| 分组 | 工具 | 能力 | +| --- | --- | --- | +| 文档创作 | Markdown 编辑器 | 编辑、工具栏、实时预览、本地文件夹读写与保存 | +| 文档创作 | Markdown ↔ HTML | 双向转换、预览、复制与下载 | +| 图像与编码 | 图片格式转换 | 单个/批量转换、PNG/JPG/WebP/GIF 选择、质量调节 | +| 图像与编码 | 二维码工具 | 文本/链接生成、尺寸与颜色设置、图片解析、下载 | +| 图像与编码 | 手绘板 | 画笔、橡皮擦、颜色/粗细调节与 PNG 导出 | +| 系统与数据 | 数据处理工具 | 正则测试、URL 编码/解码、Base64、MD5 与 SHA-256 | +| 系统与数据 | IP 地址定位 | 查询 IPv4 的地域、时区与运营商信息 | + +## 主要特点 + +- 浏览器优先:图片转换、二维码、绘图和文本处理默认在本地完成。 +- 按需加载:Markdown、二维码等较重依赖仅在进入对应工具后加载。 +- 克制的界面:响应式布局、浅色/暗色模式,以及明确的任务分组。 +- 可验证的质量门禁:单元测试、组件测试、E2E 流程、构建体积预算和设计约束检查。 +- 可直接部署:Vercel 配置已包含单页应用路由回退规则。 -一个基于 Vue 3 + Vite 的在线工具箱,采用极简编辑风重构,包含 Markdown、图片、二维码、IP 查询等工具页面。 +## 技术栈 -## 功能特点 +- [Vue 3](https://vuejs.org/) + [Vue Router](https://router.vuejs.org/) +- [Vite](https://vite.dev/) +- [Vitest](https://vitest.dev/) + [Vue Test Utils](https://test-utils.vuejs.org/) +- [Playwright](https://playwright.dev/) +- [Marked](https://marked.js.org/) + [Turndown](https://github.com/mixmark-io/turndown) +- [DOMPurify](https://github.com/cure53/DOMPurify) +- [QRCode](https://github.com/soldair/node-qrcode) + [jsQR](https://github.com/cozmo/jsQR) -- 首页工具导航 -- Markdown 编辑器(预览/实时预览、文件夹读写) -- 图片格式转换(单个与批量) -- 二维码生成与解析 -- Markdown ↔ HTML 互转 -- 数据处理(正则、URL、Base64、哈希) -- IP 地址定位、手绘板 +## 快速开始 -## 技术栈 +### 环境要求 -- Vue 3 -- Vue Router -- Marked -- Highlight.js +- Node.js 20 或更高版本 +- npm 10 或更高版本 +- 需要运行 E2E 测试时,额外安装 Playwright Chromium -## 开发 +### 本地开发 ```bash -# 安装依赖 -npm install - -# 启动开发服务器(默认 http://localhost:3000) +git clone https://github.com/Hughhhhcoder/HTooL.git +cd HTooL +npm ci npm run dev +``` -# 构建生产版本 -npm run build +开发服务器默认地址为 。 + +### 构建与预览 -# 本地预览构建产物 +```bash +npm run build npm run preview +``` + +### 运行测试 -# 单元+组件测试 +```bash +# 单元测试与组件测试 npm run test:unit -# E2E 测试(需先安装 Playwright 浏览器) -npm run test:e2e +# 设计约束检查:禁止 CSS gradient +npm run test:no-gradient # 构建产物性能预算检查 npm run test:perf-budget -# 设计约束检查(禁用 CSS 渐变) -npm run test:no-gradient +# 首次运行前安装 Playwright 浏览器 +npx playwright install chromium + +# E2E 测试 +npm run test:e2e -# 聚合检查(单测 + 构建 + 性能预算) +# 聚合检查:单测 + 设计约束 + 构建 + 性能预算 + E2E npm run check ``` ## 环境变量 -- `VITE_ENABLE_ANALYTICS`:是否在生产环境注入 Vercel Analytics(默认 `false`,设置为 `true` 才启用) +复制 `.env.example` 为 `.env` 后按需修改: + +| 变量 | 默认值 | 说明 | +| --- | --- | --- | +| `VITE_ENABLE_ANALYTICS` | `false` | 设置为 `true` 后,在生产环境注入 Vercel Analytics。 | + +## 隐私与数据边界 + +- 图片、二维码、绘图、Markdown、编码和哈希处理在浏览器中完成,项目不会因为这些功能主动上传内容到自建服务器。 +- “获取当前 IP”会请求 `api.ipify.org`;IP 归属查询会请求 `ipinfo.io`。这两个请求只在使用 IP 工具时发生。 +- Vercel Analytics 默认关闭,只有显式设置 `VITE_ENABLE_ANALYTICS=true` 才会启用。 +- Markdown 预览和 HTML 预览会经过 DOMPurify 清理;下载 HTML 前仍应审阅内容,尤其不要把不可信内容当作生产页面直接发布。 + +## 项目结构 + +```text +. +├── public/ # favicon 等静态资源 +├── scripts/ # 性能预算与设计约束检查脚本 +├── src/ +│ ├── components/ # 跨页面组件 +│ ├── utils/ # 可复用的转换与校验逻辑 +│ ├── views/ # 工具页面 +│ ├── App.vue +│ └── router/ # 路由与按需加载 +├── tests/ +│ ├── component/ # Vue 组件测试 +│ ├── e2e/ # Playwright 端到端流程 +│ └── unit/ # 工具函数与路由测试 +├── .github/ # CI、Issue/PR 模板与依赖更新配置 +└── vercel.json # SPA 部署回退配置 +``` + +## 部署 + +项目可以直接导入 Vercel: + +1. 在 Vercel 中导入本仓库。 +2. Framework Preset 选择 Vite(通常会自动识别)。 +3. Build Command 使用 `npm run build`。 +4. Output Directory 使用 `dist`。 +5. 如果需要统计,再设置 `VITE_ENABLE_ANALYTICS=true`。 + +仓库内的 `vercel.json` 已配置前端路由回退,刷新 `/markdown`、`/qrcode` 等路径时不会被当成静态文件丢失。 + +## 参与贡献 + +欢迎提交 Bug 修复、体验改进和新工具。开始之前请阅读 [CONTRIBUTING.md](./CONTRIBUTING.md),提交 Issue 时尽量提供浏览器、复现步骤和截图。 + +- [行为准则](./CODE_OF_CONDUCT.md) +- [安全策略](./SECURITY.md) +- [Issue 模板](https://github.com/Hughhhhcoder/HTooL/issues/new/choose) + +## 许可证 + +当前仓库尚未声明开源许可证。除非仓库后续补充明确的许可证文件,否则代码默认不授予复制、修改或分发权限。 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..9fde7c4 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# 安全策略 + +## 支持范围 + +目前项目没有正式发布版本,默认仅维护 `master` 分支上的最新代码。 + +| 版本/分支 | 支持情况 | +| --- | --- | +| `master` | ✅ 接受安全问题报告 | +| 其他旧提交 | ❌ 不提供修复承诺 | + +## 报告漏洞 + +请不要在公开 Issue 中发布可被利用的漏洞、攻击脚本或敏感数据。请通过 [维护者主页](https://github.com/Hughhhhcoder) 的公开联系方式私下报告,并尽量附上: + +- 影响范围和复现步骤; +- 浏览器、操作系统和项目版本; +- 最小化的复现样例; +- 你认为合理的修复建议(如果有)。 + +收到报告后,维护者会先确认问题,再评估影响范围、修复方式和公开时间。 diff --git a/index.html b/index.html index 5b6d92a..69b577b 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,20 @@ - + + + + - HTooL + + + + + + + + HTooL · 浏览器端实用工具箱
diff --git a/package-lock.json b/package-lock.json index 2600dd4..85317ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "vue-markdown-editor", + "name": "htool", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "vue-markdown-editor", + "name": "htool", "version": "1.0.0", "dependencies": { "@vercel/analytics": "^1.5.0", diff --git a/package.json b/package.json index 39fb75e..7934b11 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,11 @@ { - "name": "vue-markdown-editor", + "name": "htool", "version": "1.0.0", + "description": "HTooL — 浏览器端实用工具箱", "private": true, + "engines": { + "node": ">=20" + }, "scripts": { "dev": "vite", "build": "vite build", diff --git a/playwright.config.js b/playwright.config.js index 5d717ff..9320934 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -11,8 +11,7 @@ export default defineConfig({ { name: 'chromium', use: { - ...devices['Desktop Chrome'], - channel: 'chrome' + ...devices['Desktop Chrome'] } } ], diff --git a/src/views/MarkdownHtmlConverter.vue b/src/views/MarkdownHtmlConverter.vue index bbffa66..77bcbe6 100644 --- a/src/views/MarkdownHtmlConverter.vue +++ b/src/views/MarkdownHtmlConverter.vue @@ -85,6 +85,7 @@ const previewMode = ref('HTML') let markedLib = null let turndownService = null +let dompurifyLib = null const ensureMarked = async () => { if (markedLib) return markedLib @@ -109,10 +110,19 @@ const ensureTurndown = async () => { return turndownService } +const ensureDomPurify = async () => { + if (dompurifyLib) return dompurifyLib + const dompurifyModule = await import('dompurify') + dompurifyLib = dompurifyModule.default + return dompurifyLib +} + const convertToHtml = async () => { try { const marked = await ensureMarked() - htmlOutput.value = marked(markdownInput.value, markedRenderOptions) + const DOMPurify = await ensureDomPurify() + const html = marked(markdownInput.value, markedRenderOptions) + htmlOutput.value = DOMPurify.sanitize(html) } catch (error) { console.error('Markdown 转换错误:', error) htmlOutput.value = '
转换出错,请检查输入内容
' diff --git a/tests/component/markdownHtmlConverter.spec.js b/tests/component/markdownHtmlConverter.spec.js new file mode 100644 index 0000000..e5ec5f4 --- /dev/null +++ b/tests/component/markdownHtmlConverter.spec.js @@ -0,0 +1,19 @@ +import { mount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import MarkdownHtmlConverter from '../../src/views/MarkdownHtmlConverter.vue' + +const waitForRender = async () => { + await Promise.resolve() + await new Promise((resolve) => setTimeout(resolve, 0)) +} + +describe('MarkdownHtmlConverter view', () => { + it('sanitizes executable HTML in the Markdown preview', async () => { + const wrapper = mount(MarkdownHtmlConverter) + + await wrapper.find('textarea').setValue('') + await waitForRender() + + expect(wrapper.find('.html-preview').html()).not.toContain('onerror') + }) +}) diff --git a/tests/e2e/core-flows.spec.js b/tests/e2e/core-flows.spec.js index 64058d3..ae5ba5c 100644 --- a/tests/e2e/core-flows.spec.js +++ b/tests/e2e/core-flows.spec.js @@ -35,6 +35,14 @@ test('qr code generation flow works', async ({ page }) => { await expect(page.locator('.qr-result img')).toBeVisible() }) +test('markdown html preview removes executable HTML', async ({ page }) => { + await page.goto('/markdown-html') + await page.locator('textarea').first().fill('') + await expect(page.locator('.html-preview')).toBeVisible() + await expect(page.locator('.html-preview')).not.toContainText('onerror') + await expect(page.locator('.html-preview img')).not.toHaveAttribute('onerror') +}) + test('home supports light and dark visual captures', async ({ page }) => { await page.goto('/') const lightImage = await page.screenshot({ fullPage: true })