Skip to content

penna v0.1: 軽量クロスプラットフォーム Markdown/テキスト ビューワー#1

Merged
okash1n merged 43 commits into
mainfrom
design/penna-viewer
Jun 15, 2026
Merged

penna v0.1: 軽量クロスプラットフォーム Markdown/テキスト ビューワー#1
okash1n merged 43 commits into
mainfrom
design/penna-viewer

Conversation

@okash1n

@okash1n okash1n commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

概要

penna は、プレーンテキストと Markdown を閲覧する軽量・シンプルなクロスプラットフォーム ビューワー(読み取り専用、編集なし)です。Tauri v2(Rust + OS 標準 WebView)+ バニラ TypeScript で実装。設計(spec)→ 実装計画(plan)→ 実装、の順で進め、各タスクを実装→仕様準拠レビュー→コード品質/セキュリティレビュー→修正反映のサイクルで仕上げました。

  • 設計仕様: docs/superpowers/specs/2026-06-14-penna-viewer-design.md
  • 実装計画: docs/superpowers/plans/2026-06-14-penna-viewer-mvp.md

主なユーザー向け機能(v0.1)

  • Markdown 描画: CommonMark + GFM(表・タスクリスト・打消し線・自動リンク)+ 脚注、コードのシンタックスハイライト(highlight.js を遅延ロード)、画像(ローカル=asset protocol / リモート)
  • プレーンテキスト描画 + 文字エンコーディング自動判定(UTF-8 / UTF-16(BOM) / Shift_JIS / EUC-JP)と判定結果表示
  • ファイルを開く: CLI 引数 / Finder・Explorer ダブルクリック(拡張子関連付け)/ ドラッグ&ドロップ / File>Open メニュー。1 ファイル=1 ウィンドウ、単一インスタンス
  • テーマ OS 追従+手動トグル / ドキュメント内検索(Cmd/Ctrl+F)/ フォントズーム / 外部変更の自動リロード / 設定画面
  • セッション復元は既定 OFF(前回ウィンドウを勝手に開かない。設定で ON 可)

アーキテクチャ

単一インスタンスの Rust プロセスが 1 ファイル=1 WebView ウィンドウを管理。Rust 側がファイル I/O・エンコーディング判定・ファイル監視・設定永続化・ウィンドウ/CLI ルーティングを担当し、フロントは Markdown 描画(markdown-it → DOMPurify)・検索・テーマ・ズームを担当。フロントは生のファイルシステムに直接アクセスしません。

セキュリティ

  • 描画 HTML は DOMPurify のdeny-by-default allowlistで無害化(script/form/svg/math/style/iframe/on* を遮断、<input> は checkbox のみ、target=_blank に rel=noopener 強制、javascript: URL 除去)
  • CSP(script-src 'self' 等)/ 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 未署名

実行した検証

  • フロントエンド単体テスト(vitest + jsdom): 17 ファイル / 104 テスト 全通過
  • Rust 単体テスト(cargo test): 27 テスト 全通過
  • npm run build(tsc + vite)クリーン、cargo build クリーン(警告ゼロ)
  • 本番ビルド npm run tauri build 成功 → penna.app + penna_0.1.0_aarch64.dmg 生成(macOS arm64、ローカルのため未署名)
  • CI ワークフローは actionlint クリーン、entitlements は plutil 検証済み
  • Rust↔フロントの契約(コマンド名/引数キー/イベント名/型)整合を確認

既知の残項目(GUI 必須で自動検証不可。docs/smoke-checklist.md に手順化)

  • 実 GUI 起動・CSP/IPC の実行時挙動・ローカル画像の実表示・notify のリネーム検出 OS 依存
  • E2E(tauri-driver + WebdriverIO)は e2e/ に skip 骨子のみ(v0.1 では任意ストレッチ)

非ゴール(将来)

数式(KaTeX)/mermaid、アウトライン/目次、タブ、PDF/HTML エクスポート、自動更新、Windows/Linux 署名。詳細は CHANGELOG.md

okash1n added 30 commits June 14, 2026 22:27
…orm/style/svg, restrict input to checkbox, enforce rel=noopener
Copilot AI review requested due to automatic review settings June 15, 2026 01:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 thread src/ui/theme.ts
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 thread src/ui/theme.ts
Comment on lines +45 to +48
function detachMediaListener(): void {
if (mediaQuery && mediaListener) {
mediaQuery.removeEventListener("change", mediaListener);
}
Comment thread src/ui/find.ts
Comment on lines +63 to +65
this.currentIndex = (this.currentIndex + delta + hits.length) % hits.length;
this.setCurrent(this.currentIndex);
}
Comment thread tests/tauri-conf.test.ts
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 thread src/ui/settingsPanel.ts
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 thread src/markdown/links.ts
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 thread src-tauri/src/commands.rs
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 thread e2e/wdio.conf.ts
Comment on lines +5 to +7
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
import path from "node:path";

Comment thread src/ui/dropzone.ts

const exts = document.createElement("p");
exts.className = "drop-zone-exts";
exts.textContent = MARKDOWN_EXTENSIONS.map((e) => `.${e}`).join(" ");
@okash1n okash1n merged commit 185b08b into main Jun 15, 2026
1 check passed
@okash1n okash1n deleted the design/penna-viewer branch June 15, 2026 01:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants