penna v0.1: 軽量クロスプラットフォーム Markdown/テキスト ビューワー#1
Merged
Conversation
…g, scope should_emit to tests
…orm/style/svg, restrict input to checkbox, enforce rel=noopener
…nd escaped plaintext via sanitize
There was a problem hiding this comment.
Pull request overview
This PR introduces the initial penna v0.1 implementation: a Tauri v2 (Rust backend) + vanilla TypeScript frontend app for viewing Markdown/plain-text with security hardening, settings, and release automation.
Changes:
- Add Vite + TypeScript + Vitest setup and a frontend UI layer (rendering, theme/zoom, find-in-page, settings panel, dropzone, auto-reload).
- Add a Rust/Tauri backend for window/session management, file loading with encoding detection, file watching, settings persistence, and safe external opening.
- Add documentation, smoke checklist, and a GitHub Actions release workflow matrix.
Reviewed changes
Copilot reviewed 57 out of 114 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| vite.config.ts | Vite dev/build + Vitest configuration for Tauri. |
| tsconfig.json | TypeScript compiler configuration (strict + vitest types). |
| tests/tauri-conf.test.ts | Tests Tauri config file associations and window defaults. |
| tests/smoke.test.ts | Minimal vitest/jsdom smoke test. |
| tests/capabilities.test.ts | Tests capability defaults + asset protocol enablement. |
| src/ui/zoom.ts | Root font-size zoom controls. |
| src/ui/zoom.test.ts | Unit tests for zoom behavior and clamping. |
| src/ui/theme.ts | Theme resolution/application + font CSS variable wiring. |
| src/ui/theme.test.ts | Unit tests for theme + font behavior. |
| src/ui/settingsPanel.ts | Settings form construction + live apply + persistence callback wiring. |
| src/ui/settingsPanel.test.ts | Unit tests for settings panel control reflection and mutation. |
| src/ui/find.ts | In-document find bar implementation with match highlighting and navigation. |
| src/ui/find.test.ts | Unit tests for find counting/highlighting/navigation/close behavior. |
| src/ui/dropzone.ts | Empty-window dropzone + open dialog + drag/drop handling. |
| src/ui/dropzone.test.ts | Unit tests for drop extraction + open dialog + double-open suppression. |
| src/ui/contentClick.ts | Delegated content link click routing (external/local/anchor). |
| src/ui/contentClick.test.ts | Unit tests for click routing behavior. |
| src/ui/autoReload.ts | Frontend wiring for Tauri file-changed/file-removed events. |
| src/ui/autoReload.test.ts | Unit tests for auto-reload event wiring. |
| src/types.ts | Frontend type contracts for LoadedFile/Settings. |
| src/types.test.ts | Type-shape contract tests. |
| src/styles/theme.css | App theming, settings panel, status bar, dropzone styling. |
| src/styles/markdown.css | Markdown/plaintext styling + find-hit highlight styles. |
| src/markdown/sanitize.ts | DOMPurify deny-by-default allowlist + hooks for safe output. |
| src/markdown/sanitize.test.ts | Sanitization security/regression tests. |
| src/markdown/renderer.ts | Markdown/plaintext render pipeline (markdown-it + sanitize + img rewrite). |
| src/markdown/renderer.test.ts | Renderer unit tests (tables, tasks, footnotes, images, plaintext escaping). |
| src/markdown/markdown-it-plugins.d.ts | Local type declarations for markdown-it plugins. |
| src/markdown/links.ts | Link classification + image src resolution via asset protocol. |
| src/markdown/links.test.ts | Unit tests for link classification and resolveImageSrc behavior. |
| src/markdown/highlight.ts | Lazy highlight.js loading and code highlighting. |
| src/markdown/highlight.test.ts | Unit tests for lazy-load and per-block error isolation. |
| src/main.ts | Frontend bootstrap wiring for rendering, settings, findbar, zoom, reload, clicks. |
| src/main.test.ts | Unit tests for dirnameOf and encoding indicator wiring. |
| src/tests/security.test.ts | End-to-end-ish security tests for malicious markdown neutralization. |
| src-tauri/tauri.conf.json | Tauri app/bundle config, CSP, file associations, asset protocol enablement. |
| src-tauri/src/window.rs | Window registry, open routing, session persistence, arg parsing. |
| src-tauri/src/watcher.rs | notify-based file watcher with debounce + events. |
| src-tauri/src/settings.rs | Settings model + store load/save + serde contract tests. |
| src-tauri/src/main.rs | Tauri entry point calling library runner. |
| src-tauri/src/loader.rs | File loading + encoding detection + kind detection + serialization tests. |
| src-tauri/src/lib.rs | Tauri builder setup: plugins, menu, single-instance routing, commands. |
| src-tauri/src/commands.rs | Tauri commands for loading, settings, dialogs, opener, and window routing. |
| src-tauri/icons/android/values/ic_launcher_background.xml | Android icon background resource. |
| src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml | Android adaptive icon definition. |
| src-tauri/entitlements.plist | macOS entitlements for hardened runtime/signing configuration. |
| src-tauri/Cargo.toml | Rust/Tauri dependencies and features (protocol-asset, plugins, notify, encoding). |
| src-tauri/capabilities/default.json | Tauri v2 ACL/capabilities defaults. |
| src-tauri/build.rs | Tauri build script hook. |
| README.md | Project overview, features, dev/test/build instructions, security notes. |
| package.json | Node scripts + dependencies/devDependencies for the app/tooling. |
| index.html | App shell layout (findbar, settings mount, statusbar, content). |
| e2e/wdio.conf.ts | Optional stretch e2e config with tauri-driver/WebdriverIO wiring. |
| e2e/specs/open-render.e2e.ts | Optional stretch e2e smoke specs (currently skipped). |
| docs/superpowers/specs/2026-06-14-penna-viewer-design.md | Detailed design spec for v0.1. |
| docs/smoke-checklist.md | Manual smoke checklist for release candidates. |
| docs/RELEASE.md | Release procedure + signing/notarization notes. |
| CHANGELOG.md | v0.1.0 changelog content and roadmap. |
| .gitignore | Ignore build outputs and common local artifacts. |
| .github/workflows/release.yml | Release workflow matrix builds + signing/notarization env wiring. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+22
to
+28
| if (t === "system" && typeof matchMedia === "function") { | ||
| mediaQuery = matchMedia(DARK_QUERY); | ||
| mediaListener = () => { | ||
| document.documentElement.setAttribute("data-theme", resolveTheme("system")); | ||
| }; | ||
| mediaQuery.addEventListener("change", mediaListener); | ||
| } |
Comment on lines
+45
to
+48
| function detachMediaListener(): void { | ||
| if (mediaQuery && mediaListener) { | ||
| mediaQuery.removeEventListener("change", mediaListener); | ||
| } |
Comment on lines
+63
to
+65
| this.currentIndex = (this.currentIndex + delta + hits.length) % hits.length; | ||
| this.setCurrent(this.currentIndex); | ||
| } |
Comment on lines
+1
to
+10
| import { describe, it, expect } from "vitest"; | ||
| import { readFileSync } from "node:fs"; | ||
| import { resolve } from "node:path"; | ||
|
|
||
| const conf = JSON.parse( | ||
| readFileSync( | ||
| resolve(__dirname, "../src-tauri/tauri.conf.json"), | ||
| "utf-8", | ||
| ), | ||
| ); |
Comment on lines
+1
to
+20
| import { describe, it, expect } from "vitest"; | ||
| import { readFileSync } from "node:fs"; | ||
| import { resolve } from "node:path"; | ||
|
|
||
| const cap = JSON.parse( | ||
| readFileSync( | ||
| resolve(__dirname, "../src-tauri/capabilities/default.json"), | ||
| "utf-8", | ||
| ), | ||
| ); | ||
|
|
||
| // In Tauri v2, the asset protocol is enabled via tauri.conf.json | ||
| // (app.security.assetProtocol.enable = true) + the protocol-asset Cargo feature. | ||
| // There is no "core:asset-protocol:*" capability permission in Tauri v2's ACL. | ||
| const conf = JSON.parse( | ||
| readFileSync( | ||
| resolve(__dirname, "../src-tauri/tauri.conf.json"), | ||
| "utf-8", | ||
| ), | ||
| ); |
Comment on lines
+72
to
+82
| function readForm(): Settings { | ||
| const size = Number.parseInt(fontSize.value, 10); | ||
| return { | ||
| theme: theme.value as Settings["theme"], | ||
| sessionRestore: sessionRestore.checked, | ||
| autoReload: autoReload.checked, | ||
| fontFamily: fontFamily.value.trim() === "" ? null : fontFamily.value, | ||
| fontSize: Number.isFinite(size) ? size : BASE_FONT_SIZE, | ||
| defaultEncoding: defaultEncoding.value, | ||
| }; | ||
| } |
Comment on lines
+4
to
+8
| export function classifyLink(href: string): "external" | "anchor" | "local-file" { | ||
| if (href.startsWith("#")) return "anchor"; | ||
| if (/^https?:\/\//i.test(href) || href.startsWith("//") || href.startsWith("mailto:")) return "external"; | ||
| return "local-file"; | ||
| } |
Comment on lines
+55
to
+59
| let allowed = | ||
| url.starts_with("http://") || url.starts_with("https://") || url.starts_with("mailto:"); | ||
| if !allowed { | ||
| return Err(format!("refused to open non-external URL: {url}")); | ||
| } |
Comment on lines
+5
to
+7
| import { spawn, spawnSync, type ChildProcess } from "node:child_process"; | ||
| import path from "node:path"; | ||
|
|
|
|
||
| const exts = document.createElement("p"); | ||
| exts.className = "drop-zone-exts"; | ||
| exts.textContent = MARKDOWN_EXTENSIONS.map((e) => `.${e}`).join(" "); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概要
penna は、プレーンテキストと Markdown を閲覧する軽量・シンプルなクロスプラットフォーム ビューワー(読み取り専用、編集なし)です。Tauri v2(Rust + OS 標準 WebView)+ バニラ TypeScript で実装。設計(spec)→ 実装計画(plan)→ 実装、の順で進め、各タスクを実装→仕様準拠レビュー→コード品質/セキュリティレビュー→修正反映のサイクルで仕上げました。
docs/superpowers/specs/2026-06-14-penna-viewer-design.mddocs/superpowers/plans/2026-06-14-penna-viewer-mvp.md主なユーザー向け機能(v0.1)
アーキテクチャ
単一インスタンスの Rust プロセスが 1 ファイル=1 WebView ウィンドウを管理。Rust 側がファイル I/O・エンコーディング判定・ファイル監視・設定永続化・ウィンドウ/CLI ルーティングを担当し、フロントは Markdown 描画(markdown-it → DOMPurify)・検索・テーマ・ズームを担当。フロントは生のファイルシステムに直接アクセスしません。
セキュリティ
<input>は checkbox のみ、target=_blankに rel=noopener 強制、javascript:URL 除去)open_externalのスキーム allowlist(http/https/mailto のみ)/ asset protocol は開いたファイルのディレクトリに per-document スコープ、の多層防御対応プラットフォーム
Linux x86_64/ARM64・Windows x86_64/ARM64・macOS ARM64(Apple Silicon のみ)。配布は GitHub Actions のリリースマトリクス(
.github/workflows/release.yml)。macOS は Developer ID 署名+notarization(CI、env ゲート)、Windows/Linux は v0.1 未署名。実行した検証
npm run build(tsc + vite)クリーン、cargo buildクリーン(警告ゼロ)npm run tauri build成功 →penna.app+penna_0.1.0_aarch64.dmg生成(macOS arm64、ローカルのため未署名)既知の残項目(GUI 必須で自動検証不可。
docs/smoke-checklist.mdに手順化)e2e/に skip 骨子のみ(v0.1 では任意ストレッチ)非ゴール(将来)
数式(KaTeX)/mermaid、アウトライン/目次、タブ、PDF/HTML エクスポート、自動更新、Windows/Linux 署名。詳細は
CHANGELOG.md。