Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions packages/@d-zero/site-migrator/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# `@d-zero/site-migrator`

`.nitpicker` アーカイブを入力とするウェブサイト移植ツールキット。サブリソースのローカル DL、ページ HTML のレイアウト剥がしと BurgerEditor ブロックへの変換、`.nitpicker` DB のページメタと採番した整数 id を YAML frontmatter として prepend する CLI、同一オリジン参照を後段パイプライン向けに書き換えるリライタ、および周辺ユーティリティ関数群を提供する。
`.nitpicker` アーカイブを入力とするウェブサイト移植ツールキット。サブリソースのローカル DL、ページ HTML のレイアウト剥がしとブロック CMS 向け構造化データへの変換(既定は BurgerEditor ブロック、`BlockTargetAdapter` で差し替え可能)、`.nitpicker` DB のページメタと採番した整数 id を YAML frontmatter として prepend する CLI、同一オリジン参照を後段パイプライン向けに書き換えるリライタ、および周辺ユーティリティ関数群を提供する。

## Installation

Expand Down Expand Up @@ -39,6 +39,7 @@ import {
rewriteAssetRefs,
extractMainContent,
extractPages,
burgerEditorAdapter,
formatFrontmatter,
splitTitle,
assignPageIds,
Expand All @@ -54,7 +55,8 @@ import {

| 関数 | 概要 |
| ----------------------- | --------------------------------------------------------------------------------------------------- |
| `migrate` | アーカイブを開き、リソース DL とページ抽出を並列実行する全体フロー |
| `migrate` | アーカイブを開き、リソース DL とページ抽出を並列実行する全体フロー。`adapter` オプション必須 |
| `burgerEditorAdapter` | `migrate`/`extractPages` の `adapter` に渡す、BurgerEditor 向け既定の `BlockTargetAdapter` 実装 |
| `parseIncludePattern` | `--include` 生値 1 個を pathname プレフィックス/完全一致パターンへ解釈する純関数 |
| `filterUrlsByInclude` | `--include` 値のリストでページ URL リストを絞り込む純関数。未マッチ値があれば `IncludeNoMatchError` |
| `openArchive` | `.nitpicker` を開いてセッションを返す(要 `close()`) |
Expand All @@ -66,7 +68,7 @@ import {
| `urlToOutputPath` | URL を `<htdocs-dir>` 配下のローカルパスへ変換 |
| `rewriteAssetRefs` | HTML 内のアセット参照を resolver で書き換える(streaming) |
| `extractMainContent` | レイアウト共通部分を剥がして本文要素の `outerHTML` を返す |
| `extractPages` | ページ一覧に `extractMainContent` + `getFrontmatter` を適用して書き出す |
| `extractPages` | ページ一覧に `extractMainContent` + `getFrontmatter` を適用して書き出す。`adapter` オプション必須 |
| `formatFrontmatter` | `Frontmatter` を後段パイプライン互換の `---\n…\n---\n` YAML ブロック文字列にする |
| `splitTitle` | タイトル文字列を `|` / `\|` で分割し `{title, rawTitle?}` を返す純関数 |
| `assignPageIds` | URL リストから ディレクトリグループ採番ルールに従って `Map<url, id>` を組み立てる純関数 |
Expand All @@ -76,6 +78,21 @@ import {

`Frontmatter` の出力構造は [`./src/types.ts`](./src/types.ts) を参照。`title` / `og.title` / `twitter.title` は `|` `|` で分割して最初の非空セグメントを採用し、分割が起きたときだけ `rawTitle` 等に元文字列を保持する。

### 変換先ブロック CMS の差し替え(`BlockTargetAdapter`)

`extractPages`/`migrate` はレイアウト解析結果(anatomist 由来の `LayoutAnalysisResult`)をどう構造化データへ変換しどう HTML にレンダリングするかを一切知らず、必須オプション `adapter`(`BlockTargetAdapter<TBlocks>`)に委譲する。fetch・main 判定・anatomist 呼び出し・id 採番・frontmatter 生成・同一オリジン参照の `{{<id>}}` 解決・書き出しは変換先に依存しない共通処理として `extractPages` 自身が担う。

アダプタは 4 メソッドで構成される。

| メソッド | 必須 | 役割 |
| --------------- | ---- | ------------------------------------------------------------------------------------- |
| `classify` | 必須 | anatomist のレイアウト解析結果から `TBlocks` を組み立てる。構造化できなければ `fatal` |
| `rewriteRefs` | 必須 | `TBlocks` 内の同一オリジン URL を `pageIdLookup` で書き換える |
| `render` | 必須 | `TBlocks` を最終的なラッパー HTML 文字列へレンダリングする |
| `downloadFiles` | 任意 | コーパス全体を 1 回のバッチで扱い、ダウンロード対象アイテムの重複 DL を回避する |

BurgerEditor 向けの既定実装が `burgerEditorAdapter` で、`dz-migrate` CLI はこれを固定で使う。プログラマティック API では別のブロック CMS 向けに独自の `BlockTargetAdapter` 実装を渡せる(型と `@example` は [`./src/adapter.ts`](./src/adapter.ts) を参照)。

### `extractMainContent` のヒューリスティクス

精緻な構造推論はせず、以下の優先順位で「ページ内にちょうど 1 個だけ存在する」要素を本文として採用する。
Expand Down Expand Up @@ -137,11 +154,11 @@ import {

`extractPages` は `extractMainContent` と `getFrontmatter` を並列実行し、main 要素が見つかったページには続けて BurgerEditor ブロック変換パイプライン(後述)を適用したうえで、生成した YAML ブロックを本文の先頭に prepend してから書き出す。整数 id は常に付与されるので「DB 行なし」のページでも `---\nid: <number>\n---\n` ブロックは出る。`getFrontmatter` が例外を投げた場合は fail-soft で id-only frontmatter と本文を書き出し、`onResult` の outcome に `metaError` を載せて警告する(`migrate()` レポートでは `pagesMetaFailed` として集計される)。

### BurgerEditor ブロック変換パイプライン(`dz-migrate` のデフォルト動作
### BurgerEditor ブロック変換パイプライン(`burgerEditorAdapter` の内部実装

`.nitpicker` アーカイブベースのレイアウト剥がしだけでは `data-bge-*` マーカーが無く、BurgerEditor 上では「1 個の wysiwyg フォールバックブロック」としてしか扱えない。site-migrator の存在意義は既存サイトを BurgerEditor で編集可能なブロック構造に変換することなので、このブロック変換はオプトインフラグではなく `extractPages` / `dz-migrate` の既定動作になっている(`--content-class` は必須オプションだが、指定すれば必ず変換が走る)。
`.nitpicker` アーカイブベースのレイアウト剥がしだけでは `data-bge-*` マーカーが無く、BurgerEditor 上では「1 個の wysiwyg フォールバックブロック」としてしか扱えない。site-migrator の存在意義は既存サイトを BurgerEditor で編集可能なブロック構造に変換することなので、このブロック変換はオプトインフラグではなく `dz-migrate` の既定動作になっている(`--content-class` は必須オプションだが、指定すれば必ず変換が走る。`dz-migrate` は `adapter` に `burgerEditorAdapter` を固定で渡す)。

処理は次の要素で構成される(いずれも `src/page-extractor/` 配下、統合前は内部 API だったが `extractPages` に組み込まれた現在も `index.ts` からは export していない:
処理は次の要素で構成される(いずれも `src/page-extractor/` 配下)。`resolvePageLayouts`/`mergeMainContent`/`isMainConsistent` は `extractPages` 自身が変換先非依存の共通処理として直接呼び、`classifyBlockItem`/`layoutToBlockData`/`rewriteBlockRefs`/`renderBlocks` は前節の `burgerEditorAdapter`(`index.ts` から export 済み)が内部で呼ぶ。いずれの個別関数自体も `index.ts` からは export していない:

| 関数 | 概要 |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
Expand Down
100 changes: 100 additions & 0 deletions packages/@d-zero/site-migrator/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { DownloadResult } from './downloader/download-resources.js';
import type { PageIdLookup } from './page-extractor/rewrite-page-refs.js';
import type { LayoutAnalysisResult } from '@d-zero/anatomist/types';

/**
* {@link BlockTargetAdapter.classify}の戻り値。`main`要素は検出できたが変換先の構造化
* データを組み立てられない場合は`fatal`(呼び出し側はページ全体を無変換HTMLへフォール
* バックする)。`partial`はブロック単位の低信頼度フォールバックが一部混じっているが
* ページ全体は諦めていない状態(アダプタ実装ごとの意味は`converted`/`partial`の閾値含め
* 実装依存)。
*/
export type ClassifyResult<TBlocks> =
{ kind: 'fatal'; error: Error } | { kind: 'converted' | 'partial'; blocks: TBlocks };

export interface RewriteRefsResult<TBlocks> {
readonly blocks: TBlocks;
/**
* 個別アイテムの参照書き換えが失敗した箇所の記録(fail-soft)。どのアイテムが失敗した
* か(インデックス等)の詳細はアダプタ実装依存のため、`message`に整形済みで含める
* こと — `extractPages`側は`error.message`だけを見て1ページ分の`Error`に集約する。
*/
readonly errors: readonly Error[];
}

export interface DownloadFilesContext {
readonly outputDir: string;
/** 通常のリソースDLで既にカバー済みの絶対URL集合。二重DL回避用。 */
readonly knownResourceUrls: ReadonlySet<string>;
readonly limit?: number;
readonly signal?: AbortSignal;
readonly onResult?: (event: DownloadResult) => void;
}

/**
* nitpickerアーカイブ由来のanatomistレイアウト解析結果を、特定のブロックCMS(BurgerEditor
* 等)向けの構造化データへ変換するためのプラガブルなインターフェース。`extractPages`/
* `migrate`はこのインターフェースの向こう側の型(`TBlocks`)を一切知らず、fetch/main判定/
* anatomist呼び出し/id採番/frontmatter生成/書き出しといった変換先非依存の足回りだけを担当
* する。BurgerEditor向けの既定実装は{@link import('./page-extractor/burger-editor-adapter.js').burgerEditorAdapter}を参照。
* @example
* ```ts
* // 全ページを固定のwysiwygテキストへ倒すだけの最小アダプタ。
* const wysiwygOnlyAdapter: BlockTargetAdapter<string> = {
* classify: () => ({ kind: 'converted', blocks: 'plain text only' }),
* rewriteRefs: async (blocks) => ({ blocks, errors: [] }),
* render: async (blocks, contentClass) => `<div class="${contentClass}">${blocks}</div>`,
* };
*
* await migrate({
* archivePath: 'site.nitpicker',
* outputDir: './htdocs',
* contentClass: 'js-editable-area',
* adapter: wysiwygOnlyAdapter,
* });
* ```
*/
export interface BlockTargetAdapter<TBlocks> {
/**
* anatomistのレイアウト解析結果(同一URL・複数ビューポート分)から`TBlocks`を組み立てる。
* `main`要素の検出自体(`extractMainContent`とanatomist検出結果の整合性)は呼び出し側
* (`extractPages`)が事前にチェック済みで、ここでは渡されない — このメソッドは
* 「構造化できるかどうか」だけを判定すればよい。
* @param layoutResults
*/
classify(layoutResults: readonly LayoutAnalysisResult[]): ClassifyResult<TBlocks>;

/**
* `TBlocks`内の同一オリジンURL参照を`pageIdLookup`で書き換える(ページ参照は既知なら
* `{{<id>}}`化、それ以外はroot-relative化)。
* @param blocks
* @param baseUrl
* @param pageIdLookup
*/
rewriteRefs(
blocks: TBlocks,
baseUrl: string,
pageIdLookup: PageIdLookup,
): Promise<RewriteRefsResult<TBlocks>>;

/**
* `TBlocks`を最終的なラッパーHTML文字列へレンダリングする。呼び出し側は返り値の
* `wrapperHtml`の子要素だけを取り出し、既存main要素の子として差し替える
* (`mergeMainContent`、`contentClass`は同時にmain要素自身へ付与される)。
* @param blocks
* @param contentClass
*/
render(blocks: TBlocks, contentClass: string): Promise<string>;

/**
* 任意。コーパス全体を1回のバッチで扱い、`TBlocks`内のダウンロード対象アイテムの
* 重複DLを回避しつつ実ファイルサイズ等を書き戻す。`blocksByUrl`内のアイテムは直接
* mutateしてよい(同一オブジェクト参照のため下流に自動反映される)。省略時は何もしない。
* @param blocksByUrl
* @param ctx
*/
downloadFiles?(
blocksByUrl: ReadonlyMap<string, TBlocks>,
ctx: DownloadFilesContext,
): Promise<void>;
}
2 changes: 2 additions & 0 deletions packages/@d-zero/site-migrator/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { parseArgs } from 'node:util';

import { IncludeNoMatchError, parseIncludePattern } from './include-filter.js';
import { migrate } from './migrate.js';
import { burgerEditorAdapter } from './page-extractor/burger-editor-adapter.js';

const USAGE = `Usage:
dz-migrate <archive.nitpicker> -o <htdocs-dir> --content-class <name> [--layout-json <path>] [--limit <n>] [--extract-limit <n>] [--include <path>]...
Expand Down Expand Up @@ -130,6 +131,7 @@ async function main(argv: readonly string[]): Promise<number> {
archivePath,
outputDir,
contentClass,
adapter: burgerEditorAdapter,
layoutJsonPath,
downloadLimit,
extractLimit,
Expand Down
8 changes: 8 additions & 0 deletions packages/@d-zero/site-migrator/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
export type {
BlockTargetAdapter,
ClassifyResult,
DownloadFilesContext,
RewriteRefsResult,
} from './adapter.js';

export { openArchive } from './archive/open-archive.js';
export { listInternalPages } from './archive/list-internal-pages.js';
export { listInternalResources } from './archive/list-internal-resources.js';
Expand Down Expand Up @@ -28,6 +35,7 @@ export {
export { splitTitle } from './html/split-title.js';
export type { TitlePair } from './html/split-title.js';

export { burgerEditorAdapter } from './page-extractor/burger-editor-adapter.js';
export { extractPages } from './page-extractor/extract-pages.js';
export type {
ExtractPageItem,
Expand Down
Loading
Loading