Markdown の機能差異とリッチテキストへの移行
プリザンターの Markdown は、1 行目に [md] が必要なこと、生の HTML がすべて使えないこと、拡張構文(数式・Mermaid・脚注など)にほぼ対応していないことが、GitHub や Qiita との大きな違いです。このページでは、機能差異と移行時のチェックポイント、さらに説明項目・内容の入力列スタイルを「マークダウン」から「リッチテキストエディタ」へ SQL で一括移行する手順を説明します。
対象バージョン
機能差異の内容はバージョン 1.5.1.0 までを対象にしています。リッチテキストエディタへの移行手順はバージョン 1.4.6.0 以降が対象です。
プリザンターの Markdown の特徴
| 特徴 | 内容 |
|---|---|
| パーサー | marked.js(GFM モード。1.5.1.0 時点で v17、1.5.8.1 では v18) |
| レンダリング | クライアントサイド(ブラウザ上で変換) |
| 起動方法 | テキストの 1 行目に [md] を書く |
| HTML の扱い | 全エスケープ(生 HTML は使用不可) |
| サニタイズ | DOMPurify による多層防御 |
GitHub や Qiita がサーバーサイドでレンダリングするのに対し、プリザンターはブラウザ上の JavaScript(marked.js)で変換しています。これが機能差の背景にあります。1.5.8.1 のソースでは gfm: true・breaks: true を指定し、HTML トークンはエスケープして出力しています(markdownField.ts、package.json)。
![1 行目に [md] を書いた内容欄の表示例。見出し・番号付きリスト・表・引用・コードブロック](/images/features/040-markdown/viewer.png)
見出し・表・引用・シンタックスハイライト付きコードブロックの表示例
[md] プレフィックスが必要
Markdown として表示するには、テキストの 1 行目に [md] と書きます。
[md]
# 見出し
こんにちは **太字** です。[md] がないと「Notes モード」として扱われ、リンクと画像だけが処理されます。
確認したソースでは、先頭が [md] かどうかを indexOf('[md]') === 0 で判定し、先頭の 4 文字だけを取り除いてから変換します(markdownField.ts#L419-L421)。[md] の前に空白や空行があると Markdown になりません。[md] と同じ行に続けて書いた文字は、本文の 1 行目として扱われます。
描画の仕組み
Markdown の変換はすべてブラウザ上で行われます。サーバーは <markdown-field> というカスタム要素と、その中の <textarea> を出力するだけで、Markdig などの Markdown ライブラリは使っていません(HtmlControls.cs#L273-L339、HtmlTags.cs#L834)。
図を読み込み中…
| 部品 | 1.5.8.1 のソース | 役割 |
|---|---|---|
MarkdownFieldElement(markdown-field) | markdownField.ts | エディタとビューアの切り替え、変換、画像の貼り付け・アップロード |
| marked | ^18.0.11(package.json#L70) | Markdown → HTML |
| DOMPurify | ^3.4.14(package.json#L66) | 出力した HTML のサニタイズ |
| highlight.js | ^11.12.0(package.json#L68) | コードブロックの色付け。テーマは github-dark |
markdown-field は Shadow DOM を使わない(Light DOM の)カスタム要素なので、拡張スクリプトから document.querySelectorAll('markdown-field .md-viewer') のように中身を直接触れます。スタイルは最初の 1 つが初期化されたときに <style> として head に 1 回だけ追加されます(markdownField.ts#L648-L654)。
Markdown が使われる場所
| 場所 | 内容 |
|---|---|
内容(Body)・説明項目(Description*) | 入力列スタイルが「マークダウン」(FieldCss が field-markdown、既定値)のとき |
| コメント | textarea に data-comment が付き、空でもビューア表示になる |
| ガイド | 一覧・編集画面・カレンダーなどのガイド(GridGuide・EditorGuide・CalendarGuide など) |
| ダッシュボード | カスタムコンテンツのパーツ |
Markdown モードと Notes モード
| モード | 条件 | 処理 |
|---|---|---|
| Markdown モード | 先頭が [md] | marked の parse で変換(gfm: true・breaks: true) |
| Notes モード(既定) | それ以外 | Markdown の記号をエスケープしてから lexer にかけ、リンク・画像・テキストだけを出力して <div class="notes"> で包む |
marked のレンダラーは html・link・image・code の 4 つを差し替えています(markdownField.ts#L163-L172)。
| レンダラー | 動作 |
|---|---|
html | 生の HTML をエスケープして文字列として出す(生 HTML が使えない理由) |
link | <a href="…"> を出す。UNC パス(\\server\share\…)と notes:// は、いったんプレースホルダーに置き換えて変換し、サニタイズの後で元の URL に戻す |
image | <figure><img src="…?thumbnail=1"></figure> を出す。URL が javascript: または data: で始まるものは画像にせず、invalid-image クラスの span にする(markdownField.ts#L234-L239) |
code | highlight.js で色を付け、コピーボタン付きの div.md-code-block で包む |
Notes モードでは、コードブロックなどリンク・画像・テキスト以外のトークンは元の文字列(token.raw)がそのまま出るので、色付けもされません(markdownField.ts#L260-L282)。
サニタイズとリンクの後処理
変換した HTML は DOMPurify.sanitize(md, { ADD_ATTR: ['title'] }) を通してから表示します(markdownField.ts#L414-L442)。表示の後に次の後処理をします(markdownField.ts#L400-L412)。
- コードブロックの中(
pre code a)にできたリンクを、ただの文字に戻す。 - 画面に
#AnchorTargetBlankがあるとき(「編集画面を新しいタブで開く」が有効なとき)は、すべてのリンクにtarget="_blank"とrel="noopener noreferrer"を付ける。
DOMPurify の設定はソースに直接書かれていて、パラメータや拡張機能から変える仕組みはありません。Markdown の変換に割り込むプラグインやフックもありません。
エディタとビューアの切り替え
項目の「ビューアの切り替え」(ViewerSwitchingType)は、textarea の data-viewer-type 属性でブラウザに渡されます(Column.cs#L31-L36、HtmlControls.cs#L300-L305)。
| 設定 | data-viewer-type | 動作 |
|---|---|---|
自動(Auto) | 空(auto として扱う) | エディタからフォーカスが外れるとビューアに戻る |
手動(Manual) | manual | ビューア切り替えボタンで戻る |
無効(Disabled) | disabled | ビューアを作らず、常にエディタ。変換もしない |
どのモードでも、ビューアをダブルクリックするか編集ボタンを押すとエディタに切り替わります(Disabled を除く)。エディタ表示中は markdown-field に data-editable 属性が付き、ビューアを表示し終えると外れます(markdownField.ts#L157、#L434)。スクリプトでビューアの表示が終わったタイミングを知りたいときは、この属性の変化を MutationObserver で見ます。
サーバーが textarea に付ける属性は次のとおりです(HtmlControls.cs#L306-L336)。
| 属性・クラス | 内容 |
|---|---|
control-markdown | Markdown 項目の CSS クラス |
upload-image | 画像を挿入できるとき(項目の「画像の挿入を許可」が有効で、読み取り専用でないなど)に付く。無いと画像ボタンのある bottom-tools が消える |
data-viewer-type | 上の表のとおり |
data-comment | コメント欄 |
data-readonly | 読み取り専用 |
data-camera-disabled | モバイルで Mobile.json の EnableMobileCamera が false のとき。カメラボタンが消える |
data-enablelightbox | General.json の EnableLightBox が有効なら 1。画像をクリックするとプレビューモーダルが開く(0 なら元画像を新しいタブで開く) |
data-validate-max-length など | 最大文字数・必須・正規表現の入力チェック |
コードブロック
- 言語を指定し、highlight.js がその言語を知っていれば(
hljs.getLanguage(lang))その言語で色を付けます。言語の指定が無いか、知らない言語のときはhljs.highlightAutoで推定します(markdownField.ts#L241-L258)。mermaidも highlight.js の言語ではないので、推定で色が付いたコードとして表示されます。 - 右上のコピーボタンは
navigator.clipboard.writeTextでコードをコピーし、1.5 秒間「Copied!」を表示します(markdownField.ts#L555-L572)。
画像の挿入
画像はファイル選択ボタン・クリップボードからの貼り付け・カメラ撮影で挿入できます。binaries/uploadimage(フォームでは formbinaries/uploadimage)にアップロードされ、 の形でテキストに入ります。表示するときは URL に ?thumbnail=1 を付けてサムネイルを出します。
ブラウザ側で受け付ける形式は JPEG・PNG・GIF・WebP・BMP で、MIME タイプだけで判定します(markdownField.ts#L15、#L484-L487)。それ以外の形式は何も表示せずに無視されます。リッチテキストエディタは JPEG・PNG・GIF だけで、MIME タイプと拡張子の両方を見ます(richTextEditor.ts#L65-L66)。サーバー側のチェックとリッチテキストエディタで WebP・BMP を使えるようにする改修は、エディタの画像形式を増やす にまとめています。
他サービスとの比較
見出し、太字、リスト、テーブル、コードブロック、引用、タスクリスト、リンク・画像といった基本構文は、プリザンターを含むどのサービスでも問題なく使えます。差が出るのは次の点です。
改行
| 挙動 | Pleasanter | GitHub | Qiita | Zenn | GitLab |
|---|---|---|---|---|---|
単一改行 → <br> | 対応 | 非対応 | 対応 | 対応 | 非対応 |
末尾 2 スペース + 改行 → <br> | 対応 | 対応 | 対応 | 対応 | 対応 |
プリザンターでは Enter キーを 1 回押しただけの改行がそのまま改行として表示されます(breaks: true 相当)。GitHub・GitLab では 1 回の改行はスペースとして扱われます(CommonMark の仕様)。そのため、プリザンターから GitHub へ移すときは改行が消える可能性があります。
生の HTML(最大の違い)
| 構文 | Pleasanter | GitHub | Qiita | Zenn | GitLab |
|---|---|---|---|---|---|
| インライン HTML | 非対応 | 対応 | 一部対応 | 一部対応 | 対応 |
| ブロックレベル HTML | 非対応 | 対応 | 一部対応 | 非対応 | 対応 |
| HTML コメント | 非対応 | 対応 | 対応 | 対応 | 対応 |
プリザンターではすべての HTML タグがエスケープされます。次のような記法は、タグごとそのまま文字列として表示されます。
<!-- これはコメント(表示されない)-->
<details><summary>クリックで展開</summary>
隠れる内容
</details>
<kbd>Ctrl</kbd> + <kbd>C</kbd>
文字の<sup>上付き</sup>と<sub>下付き</sub>全 HTML エスケープと DOMPurify の多層防御により、セキュリティ面では最も厳格な設計です。
拡張構文
| 構文 | Pleasanter | GitHub | Qiita | Zenn | GitLab |
|---|---|---|---|---|---|
インライン数式 $...$ | 非対応 | 対応 | |||
ブロック数式 $$...$$ | 非対応 | 対応 | |||
| Mermaid | 非対応 | 対応 | 非対応 | 非対応 | 対応 |
脚注([^1]) | 非対応 | 対応 | 対応 | 対応 | |
| アラート / Note 記法 | 非対応 | 対応 | 対応 | 対応 | |
| 絵文字ショートコード | 非対応 | 対応 | 対応 | 対応 | |
| ユーザーメンション | 非対応 | 対応 | 対応 | 対応 | |
| 見出しの自動アンカー | 非対応 | 対応 | 対応 | 対応 |
プリザンターには Mermaid のライブラリ(wwwroot/Extensions/mermaid-11.9.0.min.js)が同梱されていますが、これはサイト設定の可視化(Site Visualizer)の ER 図などを描くためのもので、そのページでしか読み込まれません(smt-json-to-table.html#L306)。Markdown フィールドからは参照されないので、```mermaid と書いてもコードブロックとして表示されます。拡張構文を使った Markdown を持ち込んでも表示されないので、基本構文だけで書いてください。Mermaid などを描画できるようにする方法は Markdown フィールドの拡張(Mermaid・独自記法) にまとめています。
シンタックスハイライト
| 項目 | Pleasanter | GitHub | Qiita |
|---|---|---|---|
| エンジン | highlight.js | Linguist ベース | Rouge / 独自 |
| 対応言語数 | highlight.js の全言語(約 190) | 数百言語 | 多数 |
| 言語未指定時 | 自動検出 | ハイライトなし | ハイライトなし |
| ファイル名表示 | 非対応 | 非対応 | 対応 |
| コピーボタン | 対応 |
言語を指定しなくても highlightAuto(自動検出)が働きますが、意図しない言語で色が付くことがあるため、言語は指定しておくほうが確実です。1.5.8.1 のソースでは import hljs from 'highlight.js' で全言語入りのビルドを読み込み、言語未指定時は hljs.highlightAuto を使っています(markdownField.ts)。Qiita の「ファイル名付きコードブロック」(```ruby:app.rb)には対応していません。
プリザンター独自の機能
| 機能 | 説明 |
|---|---|
| UNC パスの自動リンク | \\server\share\path を file:// リンクに自動変換 |
| IBM Notes リンク | notes:// プロトコルをそのままリンク化 |
| サムネイル・ライトボックス | 画像クリックでモーダル拡大表示 |
| カメラ撮影 | モバイルからカメラ撮影して画像を直接挿入 |
| ビューア切替 | エディタ ↔ プレビューの表示モードを Auto / Manual / Disabled で切替 |
target="_blank" リンク | 外部リンクを新しいタブで開く設定 |
移行時のチェックリスト
他サービス → プリザンター
- 1 行目に
[md]を追加する - 生 HTML(
details、sup、kbdなどのタグ)を削除・代替する - 数式(
$...$)やダイアグラム(Mermaid)を画像などに置き換える - サービス固有の拡張構文(
:::note、> [!NOTE]など)を除去する - 絵文字ショートコード(
:smile:)を Unicode 絵文字に変換する - ファイル名付きコードブロック(
```ruby:app.rb)の:ファイル名部分を除去する
プリザンター → 他サービス
[md]を削除する- 改行の表示を確認する(GitHub / GitLab では単一改行が反映されない)
- UNC パス(
\\server\share)や Notes リンクを手動でリンクに変換する - 画像の
?thumbnail=1パラメータを除去する
マークダウンエディタからリッチテキストエディタへ一括移行する 1.4.6.0 以降
説明項目(Description)と内容(Body)の入力列スタイルは、テーブルの管理画面から 1 つずつ変更できますが、対象が多い場合は SQL で一括変更できます。SQL はすべて各データベースの組込み JSON 関数を使います。
DANGER
データベースを直接操作する方法です。実行前に必ずバックアップを取得し、テスト環境で検証してから本番に適用してください。
マークダウンとリッチテキストの違い
| 項目 | マークダウン | リッチテキスト |
|---|---|---|
| 入力方式 | テキストベース(記法を覚える必要あり) | WYSIWYG |
| データ形式 | プレーンテキスト([md] プレフィックス付き) | HTML |
| フィルタ・ソート | 対応 | テキストとして扱われる(HTML タグ込み) |
| CSV エクスポート | テキストそのまま | HTML タグ込み |
| 画像挿入 | 対応 | 対応 |
| 表の作成 | Markdown テーブル記法 | ツールバーから挿入 |
設定値と保存形式
入力列スタイルは、Sites.SiteSettings の JSON の中で、各カラムの FieldCss プロパティとして管理されています。1.5.8.1 のソースで選択肢として定義されている値は次のとおりです(SiteUtilities.cs)。
| 入力列スタイル | FieldCss の値 |
|---|---|
| ノーマル | "field-normal" |
| ワイド | "field-wide" |
| マークダウン | "field-markdown"(既定値) |
| リッチテキストエディタ | "field-rte" |
説明項目と内容の既定値は "field-markdown" です(Description.json、_BaseItems_Body.json)。SiteSettings には既定値と異なるプロパティだけが保存され(SiteSettings.cs)、既定値と異なる設定が 1 つもないカラムは Columns 配列に要素自体が作られません(SiteSettings.cs)。そのため、マークダウンのカラムは次のどちらかの状態になっています。
Columns配列に要素があり、FieldCssプロパティがないColumns配列に要素がない
リッチテキストエディタに変えたカラムだけが "FieldCss": "field-rte" を持ちます。
マークダウンエディタの場合、データの先頭に [md] が付きます(クライアントサイドで Markdown レンダリングを起動するためのフラグ)。リッチテキストエディタの場合は HTML で保存されます。
[md]
# 見出し
これは **太字** のテキストです。
- リスト1
- リスト2<h1>見出し</h1>
<p>これは <strong>太字</strong> のテキストです。</p>
<ul>
<li>リスト1</li>
<li>リスト2</li>
</ul>ステップ 1: 対象のサイトと項目を調べる
記録テーブル・期限付きテーブルのサイトについて、Columns 配列にある内容(Body)と説明項目(Description*)の要素と、その FieldCss を一覧にします。FieldCss が NULL の行はマークダウンです。この一覧に出てこないカラムも、要素がないだけでマークダウンとして扱われます。結果を確認して対象範囲を絞り込んでから次に進みます。
SELECT
s.[SiteId],
s.[Title],
col.[ColumnName],
col.[FieldCss]
FROM
[Sites] s
CROSS APPLY OPENJSON(s.[SiteSettings], '$.Columns')
WITH (
[ColumnName] NVARCHAR(100) '$.ColumnName',
[FieldCss] NVARCHAR(100) '$.FieldCss'
) col
WHERE
s.[ReferenceType] IN ('Issues', 'Results')
AND (col.[ColumnName] = 'Body' OR col.[ColumnName] LIKE 'Description%')
ORDER BY
s.[SiteId],
col.[ColumnName];SELECT
s."SiteId",
s."Title",
col->>'ColumnName' AS "ColumnName",
col->>'FieldCss' AS "FieldCss"
FROM
"Sites" s,
jsonb_array_elements(s."SiteSettings"::jsonb -> 'Columns') AS col
WHERE
s."ReferenceType" IN ('Issues', 'Results')
AND (col ->> 'ColumnName' = 'Body' OR col ->> 'ColumnName' LIKE 'Description%')
ORDER BY
s."SiteId",
col->>'ColumnName';SELECT
s.`SiteId`,
s.`Title`,
col.`ColumnName`,
col.`FieldCss`
FROM
`Sites` s,
JSON_TABLE(
s.`SiteSettings`,
'$.Columns[*]' COLUMNS (
`ColumnName` VARCHAR(100) PATH '$.ColumnName',
`FieldCss` VARCHAR(100) PATH '$.FieldCss'
)
) col
WHERE
s.`ReferenceType` IN ('Issues', 'Results')
AND (col.`ColumnName` = 'Body' OR col.`ColumnName` LIKE 'Description%')
ORDER BY
s.`SiteId`,
col.`ColumnName`;ステップ 2: SiteSettings の FieldCss を書き換える
カラムごとに、FieldCss を "field-rte" にします。マークダウンのカラムには上の 2 つの状態があるため、次の 2 つのクエリを両方実行します。どちらも DescriptionA を対象にする例です。ほかのカラムは DescriptionA をそのカラム名(Body、DescriptionB など)に置き換えて実行してください。特定のサイトだけを対象にする場合は、WHERE に SiteId の条件を追加します。
INFO
FieldCss の値を "control-markdown" / "control-markup" にしても動きません。control-markdown は画面部品の CSS クラス名で、FieldCss の値ではありません。上の値は 1.5.8.1 のソースに合わせています。
2-1. 要素があり FieldCss がないカラムを書き換える
UPDATE s
SET s.[SiteSettings] = JSON_MODIFY(
s.[SiteSettings],
'$.Columns[' + col.[key] + '].FieldCss',
'field-rte'
)
FROM [Sites] s
CROSS APPLY (
SELECT TOP 1 [key]
FROM OPENJSON(s.[SiteSettings], '$.Columns')
WHERE JSON_VALUE([value], '$.ColumnName') = 'DescriptionA'
AND JSON_VALUE([value], '$.FieldCss') IS NULL
) col
WHERE s.[ReferenceType] IN ('Issues', 'Results');UPDATE "Sites" s
SET "SiteSettings" = jsonb_set(
s."SiteSettings"::jsonb,
ARRAY['Columns', sub.idx::text, 'FieldCss'],
'"field-rte"'
)::text
FROM (
SELECT s2."SiteId", (e.ordinality - 1) AS idx
FROM "Sites" s2,
jsonb_array_elements(s2."SiteSettings"::jsonb -> 'Columns')
WITH ORDINALITY AS e(value, ordinality)
WHERE e.value ->> 'ColumnName' = 'DescriptionA'
AND NOT (e.value ? 'FieldCss')
) sub
WHERE s."SiteId" = sub."SiteId"
AND s."ReferenceType" IN ('Issues', 'Results');UPDATE `Sites` s
INNER JOIN (
SELECT
s2.`SiteId`,
col.`idx`
FROM
`Sites` s2,
JSON_TABLE(
s2.`SiteSettings`,
'$.Columns[*]' COLUMNS (
`idx` FOR ORDINALITY,
`ColumnName` VARCHAR(100) PATH '$.ColumnName',
`FieldCss` VARCHAR(100) PATH '$.FieldCss'
)
) col
WHERE
col.`ColumnName` = 'DescriptionA'
AND col.`FieldCss` IS NULL
) t ON s.`SiteId` = t.`SiteId`
SET s.`SiteSettings` = JSON_SET(
s.`SiteSettings`,
CONCAT('$.Columns[', t.`idx` - 1, '].FieldCss'),
'field-rte'
)
WHERE s.`ReferenceType` IN ('Issues', 'Results');2-2. 要素がないカラムを追加する
Columns 配列に {"ColumnName":"DescriptionA","FieldCss":"field-rte"} の要素を追加します。読み込み時に、ここにないプロパティはカラム定義の既定値で補われます。
UPDATE [Sites]
SET [SiteSettings] = JSON_MODIFY(
[SiteSettings],
'append $.Columns',
JSON_QUERY('{"ColumnName":"DescriptionA","FieldCss":"field-rte"}')
)
WHERE [ReferenceType] IN ('Issues', 'Results')
AND NOT EXISTS (
SELECT 1
FROM OPENJSON([SiteSettings], '$.Columns')
WHERE JSON_VALUE([value], '$.ColumnName') = 'DescriptionA'
);UPDATE "Sites"
SET "SiteSettings" = jsonb_set(
"SiteSettings"::jsonb,
'{Columns}',
COALESCE("SiteSettings"::jsonb -> 'Columns', '[]'::jsonb)
|| '[{"ColumnName":"DescriptionA","FieldCss":"field-rte"}]'::jsonb
)::text
WHERE "ReferenceType" IN ('Issues', 'Results')
AND NOT COALESCE("SiteSettings"::jsonb -> 'Columns', '[]'::jsonb)
@> '[{"ColumnName":"DescriptionA"}]';UPDATE `Sites`
SET `SiteSettings` = JSON_SET(
`SiteSettings`,
'$.Columns',
JSON_ARRAY_APPEND(
COALESCE(JSON_EXTRACT(`SiteSettings`, '$.Columns'), JSON_ARRAY()),
'$',
JSON_OBJECT('ColumnName', 'DescriptionA', 'FieldCss', 'field-rte')
)
)
WHERE `ReferenceType` IN ('Issues', 'Results')
AND NOT JSON_CONTAINS(
COALESCE(JSON_EXTRACT(`SiteSettings`, '$.Columns'), JSON_ARRAY()),
JSON_OBJECT('ColumnName', 'DescriptionA')
);WARNING
FieldCss が "field-normal" や "field-wide" のカラムは、どちらのクエリでも変更されません。2-2 は条件に合うすべてのサイトに要素を追加するので、使っていないカラムの要素も作られます(表示には影響しません)。気になる場合はステップ 1 で確認したサイトの SiteId で絞り込んでください。
ステップ 3: 保存済みデータの [md] を除去する
SiteSettings を変更しただけでは、既存レコードの [md] 付きテキストがそのまま残ります。変換方針は 2 つあります。
| 方針 | メリット | デメリット |
|---|---|---|
[md] プレフィックスだけ除去する | SQL だけで完結。シンプル | Markdown 記法がプレーンテキストとして残る |
| Markdown → HTML に変換する | リッチテキストエディタで正しく表示される | 外部ツールが必要 |
ここではまず [md] を除去します。HTML への変換が必要な場合は 続くステップ 4 の方法を使ってください。
対象になるのは次のカラムです。
| カラム | 保存先テーブル | カラム名パターン |
|---|---|---|
| Description(説明) | Issues / Results | DescriptionA 〜 DescriptionZ |
| Body(内容) | Issues / Results | Body |
INFO
Enterprise Edition の項目拡張を使っている環境では、Description001 〜 のカラムも対象になります。環境に合わせてカラム名を追加してください。
WARNING
[md] の後に改行(\n)が 1 つ入っているため、除去する文字数は 5 文字([md]\n)です。ただし [md] の直後に改行なしでテキストが続いている場合もあるので、実際のデータを確認してから実行してください。
SQL Server の LIKE では [md] が「m か d の 1 文字」という意味になるため、[[]md] とエスケープしています。
Issues テーブルの DescriptionA の例です。ステップ 2 で field-rte に変更済みのサイトだけを対象にしています。
-- Issues テーブルの DescriptionA から [md] プレフィックスを除去
UPDATE i
SET i.[DescriptionA] = SUBSTRING(i.[DescriptionA], 6, LEN(i.[DescriptionA]))
FROM [Issues] i
WHERE i.[SiteId] IN (
SELECT s.[SiteId]
FROM [Sites] s
CROSS APPLY OPENJSON(s.[SiteSettings], '$.Columns')
WITH (
[ColumnName] NVARCHAR(100) '$.ColumnName',
[FieldCss] NVARCHAR(100) '$.FieldCss'
) col
WHERE col.[ColumnName] = 'DescriptionA'
AND col.[FieldCss] = 'field-rte'
)
AND i.[DescriptionA] LIKE '[[]md]' + CHAR(10) + '%';-- Issues テーブルの DescriptionA から [md] プレフィックスを除去
UPDATE "Issues"
SET "DescriptionA" = SUBSTRING("DescriptionA" FROM 6)
WHERE "SiteId" IN (
SELECT s."SiteId"
FROM "Sites" s,
jsonb_array_elements(s."SiteSettings"::jsonb -> 'Columns') AS col
WHERE col ->> 'ColumnName' = 'DescriptionA'
AND col ->> 'FieldCss' = 'field-rte'
)
AND "DescriptionA" LIKE '[md]' || CHR(10) || '%';-- Issues テーブルの DescriptionA から [md] プレフィックスを除去
UPDATE `Issues`
SET `DescriptionA` = SUBSTRING(`DescriptionA`, 6)
WHERE `SiteId` IN (
SELECT s.`SiteId`
FROM `Sites` s,
JSON_TABLE(
s.`SiteSettings`,
'$.Columns[*]' COLUMNS (
`ColumnName` VARCHAR(100) PATH '$.ColumnName',
`FieldCss` VARCHAR(100) PATH '$.FieldCss'
)
) col
WHERE col.`ColumnName` = 'DescriptionA'
AND col.`FieldCss` = 'field-rte'
)
AND `DescriptionA` LIKE CONCAT('[md]', CHAR(10 USING utf8mb4), '%');DescriptionB以降が対象の場合は、クエリ内のDescriptionAをそれぞれのカラム名に変えて実行します。Bodyも同じクエリで、カラム名とColumnNameの条件をBodyに変えて実行します。Resultsテーブルも、テーブル名を置き換えて同様に実行します。
ステップ 4: Markdown を HTML に変換する
リッチテキストエディタで見出しや太字、リストを正しく表示するには、Markdown を HTML に変換します。変換には C# スクリプト(Markdig)を推奨します。Markdig は .NET 向けの Markdown パーサーで、PowerShell の ConvertFrom-Markdown よりも GFM との互換性が高い変換ができます。なお、1.5.8.1 のソースには Markdig への参照はなく、プリザンター本体の描画はクライアントサイドの marked で行われています。
C# スクリプト(Markdig)
dotnet-script を使って .csx ファイルとして実行します。冒頭の「設定」で接続文字列・テーブル名・カラム名を編集してから実行してください。
// Convert-MarkdownToHtml.csx — SQL Server 向け
#r "nuget: Markdig, 0.38.0"
#r "nuget: Microsoft.Data.SqlClient, 5.2.2"
using Markdig;
using Microsoft.Data.SqlClient;
// ---- 設定 ----
var connStr = "Server=localhost;Database=Implem.Pleasanter;Trusted_Connection=true;TrustServerCertificate=true";
var tableName = "Issues"; // Issues or Results
var columnName = "DescriptionA"; // DescriptionA, Body, etc.
// --------------
var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
var idCol = tableName == "Issues" ? "IssueId" : "ResultId";
using var conn = new SqlConnection(connStr);
conn.Open();
var selectSql = $@"
SELECT [{idCol}], [{columnName}]
FROM [{tableName}]
WHERE [SiteId] IN (
SELECT s.[SiteId]
FROM [Sites] s
CROSS APPLY OPENJSON(s.[SiteSettings], '$.Columns')
WITH (
[ColumnName] NVARCHAR(100) '$.ColumnName',
[FieldCss] NVARCHAR(100) '$.FieldCss'
) c
WHERE c.[ColumnName] = '{columnName}'
AND c.[FieldCss] = 'field-rte'
)
AND [{columnName}] IS NOT NULL
AND [{columnName}] <> ''";
var rows = new List<(long id, string md)>();
using (var cmd = new SqlCommand(selectSql, conn))
using (var r = cmd.ExecuteReader())
while (r.Read())
rows.Add((r.GetInt64(0), r.GetString(1)));
var converted = 0;
foreach (var (id, raw) in rows)
{
var md = raw;
if (md.StartsWith("[md]\n")) md = md.Substring(5);
else if (md.StartsWith("[md]\r\n")) md = md.Substring(6);
var html = Markdown.ToHtml(md, pipeline);
using var u = new SqlCommand(
$"UPDATE [{tableName}] SET [{columnName}]=@h WHERE [{idCol}]=@id", conn);
u.Parameters.AddWithValue("@h", html);
u.Parameters.AddWithValue("@id", id);
u.ExecuteNonQuery();
converted++;
}
Console.WriteLine($"{converted} 件のレコードを変換しました。");// Convert-MarkdownToHtml.csx — PostgreSQL 向け
#r "nuget: Markdig, 0.38.0"
#r "nuget: Npgsql, 8.0.5"
using Markdig;
using Npgsql;
// ---- 設定 ----
var connStr = "Host=localhost;Database=Implem.Pleasanter;Username=postgres;Password=yourpassword";
var tableName = "Issues"; // Issues or Results
var columnName = "DescriptionA"; // DescriptionA, Body, etc.
// --------------
var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
var idCol = tableName == "Issues" ? "IssueId" : "ResultId";
using var conn = new NpgsqlConnection(connStr);
conn.Open();
var selectSql = $@"
SELECT ""{idCol}"", ""{columnName}""
FROM ""{tableName}""
WHERE ""SiteId"" IN (
SELECT s.""SiteId""
FROM ""Sites"" s,
jsonb_array_elements(s.""SiteSettings""::jsonb -> 'Columns') AS col
WHERE col ->> 'ColumnName' = '{columnName}'
AND col ->> 'FieldCss' = 'field-rte'
)
AND ""{columnName}"" IS NOT NULL
AND ""{columnName}"" <> ''";
var rows = new List<(long id, string md)>();
using (var cmd = new NpgsqlCommand(selectSql, conn))
using (var r = cmd.ExecuteReader())
while (r.Read())
rows.Add((r.GetInt64(0), r.GetString(1)));
var converted = 0;
foreach (var (id, raw) in rows)
{
var md = raw;
if (md.StartsWith("[md]\n")) md = md.Substring(5);
else if (md.StartsWith("[md]\r\n")) md = md.Substring(6);
var html = Markdown.ToHtml(md, pipeline);
using var u = new NpgsqlCommand(
$@"UPDATE ""{tableName}"" SET ""{columnName}""=@h WHERE ""{idCol}""=@id", conn);
u.Parameters.AddWithValue("h", html);
u.Parameters.AddWithValue("id", id);
u.ExecuteNonQuery();
converted++;
}
Console.WriteLine($"{converted} 件のレコードを変換しました。");// Convert-MarkdownToHtml.csx — MySQL 向け
#r "nuget: Markdig, 0.38.0"
#r "nuget: MySqlConnector, 2.4.0"
using Markdig;
using MySqlConnector;
// ---- 設定 ----
var connStr = "Server=localhost;Database=Implem.Pleasanter;User=root;Password=yourpassword";
var tableName = "Issues"; // Issues or Results
var columnName = "DescriptionA"; // DescriptionA, Body, etc.
// --------------
var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
var idCol = tableName == "Issues" ? "IssueId" : "ResultId";
using var conn = new MySqlConnection(connStr);
conn.Open();
var selectSql = $@"
SELECT {idCol}, {columnName}
FROM {tableName}
WHERE SiteId IN (
SELECT s.SiteId
FROM Sites s,
JSON_TABLE(
s.SiteSettings,
'$.Columns[*]' COLUMNS (
ColumnName VARCHAR(100) PATH '$.ColumnName',
FieldCss VARCHAR(100) PATH '$.FieldCss'
)
) col
WHERE col.ColumnName = '{columnName}'
AND col.FieldCss = 'field-rte'
)
AND {columnName} IS NOT NULL
AND {columnName} <> ''";
var rows = new List<(long id, string md)>();
using (var cmd = new MySqlCommand(selectSql, conn))
using (var r = cmd.ExecuteReader())
while (r.Read())
rows.Add((r.GetInt64(0), r.GetString(1)));
var converted = 0;
foreach (var (id, raw) in rows)
{
var md = raw;
if (md.StartsWith("[md]\n")) md = md.Substring(5);
else if (md.StartsWith("[md]\r\n")) md = md.Substring(6);
var html = Markdown.ToHtml(md, pipeline);
using var u = new MySqlCommand(
$"UPDATE {tableName} SET {columnName}=@h WHERE {idCol}=@id", conn);
u.Parameters.AddWithValue("@h", html);
u.Parameters.AddWithValue("@id", id);
u.ExecuteNonQuery();
converted++;
}
Console.WriteLine($"{converted} 件のレコードを変換しました。");実行方法は次のとおりです(.NET SDK がインストール済みの前提)。
dotnet tool install -g dotnet-script
dotnet script Convert-MarkdownToHtml.csxINFO
UseAdvancedExtensions() を指定すると、テーブル記法、タスクリスト、パイプテーブルなどの拡張記法もサポートされます。ただし、プリザンター本体の Markdown 描画はクライアントサイドの marked(JavaScript)で行われているため、Markdig とレンダリング結果が完全に一致するわけではありません。変換後は実データで表示を確認してください。
PowerShell(ConvertFrom-Markdown)
PowerShell 7 以降の ConvertFrom-Markdown でも変換できます。PowerShell 5.1(Windows PowerShell)では使えません。SQL Server 向けの例を示します(SqlServer モジュールが必要です)。PostgreSQL 向け(Npgsql を Add-Type で読み込む)と MySQL 向け(MySqlConnector を Add-Type で読み込む)のスクリプトは、その下に載せています。
# SQL Server: Markdown → HTML 変換スクリプト
# 要件: PowerShell 7以降、SqlServer モジュール
param(
[string]$ServerInstance = "localhost",
[string]$Database = "Implem.Pleasanter",
[string]$TableName = "Issues", # Issues or Results
[string]$ColumnName = "DescriptionA" # DescriptionA, Body, etc.
)
Import-Module SqlServer
# 対象レコードを取得
$query = @"
SELECT
i.[$($TableName -eq 'Issues' ? 'IssueId' : 'ResultId')] AS [RecordId],
i.[$ColumnName] AS [MarkdownText]
FROM [$TableName] i
WHERE i.[SiteId] IN (
SELECT s.[SiteId]
FROM [Sites] s
CROSS APPLY OPENJSON(s.[SiteSettings], '$.Columns')
WITH (
[ColumnName] NVARCHAR(100) '$.ColumnName',
[FieldCss] NVARCHAR(100) '$.FieldCss'
) col
WHERE col.[ColumnName] = '$ColumnName'
AND col.[FieldCss] = 'field-rte'
)
AND i.[$ColumnName] IS NOT NULL
AND i.[$ColumnName] <> ''
AND i.[$ColumnName] NOT LIKE '<%'
"@
$rows = Invoke-Sqlcmd -ServerInstance $ServerInstance -Database $Database -Query $query
$idColumn = if ($TableName -eq 'Issues') { 'IssueId' } else { 'ResultId' }
$converted = 0
foreach ($row in $rows) {
$md = $row.MarkdownText
# [md] プレフィックスが残っている場合は除去
if ($md -match '^\[md\]\r?\n') {
$md = $md -replace '^\[md\]\r?\n', ''
}
# Markdown → HTML 変換
$html = (ConvertFrom-Markdown -InputObject $md).Html
# データベースを更新
$updateQuery = @"
UPDATE [$TableName]
SET [$ColumnName] = @html
WHERE [$idColumn] = @id
"@
Invoke-Sqlcmd -ServerInstance $ServerInstance -Database $Database `
-Query $updateQuery `
-Variable "html=$html", "id=$($row.RecordId)"
$converted++
}
Write-Host "$converted 件のレコードを変換しました。" -ForegroundColor GreenPostgreSQL 向け(Npgsql)
# PostgreSQL: Markdown → HTML 変換スクリプト
# 要件: PowerShell 7以降
param(
[string]$ConnectionString = "Host=localhost;Database=Implem.Pleasanter;Username=postgres;Password=yourpassword",
[string]$TableName = "Issues", # Issues or Results
[string]$ColumnName = "DescriptionA" # DescriptionA, Body, etc.
)
# Npgsql をロード(NuGetから取得済みの前提)
Add-Type -Path "path/to/Npgsql.dll"
$idColumn = if ($TableName -eq 'Issues') { 'IssueId' } else { 'ResultId' }
$conn = [Npgsql.NpgsqlConnection]::new($ConnectionString)
$conn.Open()
# 対象レコードを取得
$selectSql = @"
SELECT
"$idColumn" AS "RecordId",
"$ColumnName" AS "MarkdownText"
FROM "$TableName"
WHERE "SiteId" IN (
SELECT s."SiteId"
FROM "Sites" s,
jsonb_array_elements(s."SiteSettings"::jsonb -> 'Columns') AS col
WHERE col ->> 'ColumnName' = '$ColumnName'
AND col ->> 'FieldCss' = 'field-rte'
)
AND "$ColumnName" IS NOT NULL
AND "$ColumnName" <> ''
AND "$ColumnName" NOT LIKE '<%'
"@
$cmd = $conn.CreateCommand()
$cmd.CommandText = $selectSql
$reader = $cmd.ExecuteReader()
$records = @()
while ($reader.Read()) {
$records += @{
RecordId = $reader["RecordId"]
MarkdownText = $reader["MarkdownText"]
}
}
$reader.Close()
$converted = 0
foreach ($rec in $records) {
$md = $rec.MarkdownText
if ($md -match '^\[md\]\r?\n') {
$md = $md -replace '^\[md\]\r?\n', ''
}
$html = (ConvertFrom-Markdown -InputObject $md).Html
$updateCmd = $conn.CreateCommand()
$updateCmd.CommandText = "UPDATE ""$TableName"" SET ""$ColumnName"" = @html WHERE ""$idColumn"" = @id"
$updateCmd.Parameters.AddWithValue("html", $html) | Out-Null
$updateCmd.Parameters.AddWithValue("id", $rec.RecordId) | Out-Null
$updateCmd.ExecuteNonQuery() | Out-Null
$converted++
}
$conn.Close()
Write-Host "$converted 件のレコードを変換しました。" -ForegroundColor GreenMySQL 向け(MySqlConnector)
# MySQL: Markdown → HTML 変換スクリプト
# 要件: PowerShell 7以降、MySqlConnector
param(
[string]$ConnectionString = "Server=localhost;Database=Implem.Pleasanter;User=root;Password=yourpassword",
[string]$TableName = "Issues", # Issues or Results
[string]$ColumnName = "DescriptionA" # DescriptionA, Body, etc.
)
# MySqlConnector をロード(NuGetから取得済みの前提)
Add-Type -Path "path/to/MySqlConnector.dll"
$idColumn = if ($TableName -eq 'Issues') { 'IssueId' } else { 'ResultId' }
$conn = [MySqlConnector.MySqlConnection]::new($ConnectionString)
$conn.Open()
# 対象レコードを取得
$selectSql = @"
SELECT
$idColumn AS RecordId,
$ColumnName AS MarkdownText
FROM $TableName
WHERE SiteId IN (
SELECT s.SiteId
FROM Sites s,
JSON_TABLE(
s.SiteSettings,
'$.Columns[*]' COLUMNS (
ColumnName VARCHAR(100) PATH '$.ColumnName',
FieldCss VARCHAR(100) PATH '$.FieldCss'
)
) col
WHERE col.ColumnName = '$ColumnName'
AND col.FieldCss = 'field-rte'
)
AND $ColumnName IS NOT NULL
AND $ColumnName <> ''
AND $ColumnName NOT LIKE '<%'
"@
$cmd = [MySqlConnector.MySqlCommand]::new($selectSql, $conn)
$reader = $cmd.ExecuteReader()
$records = @()
while ($reader.Read()) {
$records += @{
RecordId = $reader["RecordId"]
MarkdownText = $reader["MarkdownText"]
}
}
$reader.Close()
$converted = 0
foreach ($rec in $records) {
$md = $rec.MarkdownText
if ($md -match '^\[md\]\r?\n') {
$md = $md -replace '^\[md\]\r?\n', ''
}
$html = (ConvertFrom-Markdown -InputObject $md).Html
$updateCmd = [MySqlConnector.MySqlCommand]::new(
"UPDATE $TableName SET $ColumnName = @html WHERE $idColumn = @id", $conn)
$updateCmd.Parameters.AddWithValue("@html", $html) | Out-Null
$updateCmd.Parameters.AddWithValue("@id", $rec.RecordId) | Out-Null
$updateCmd.ExecuteNonQuery() | Out-Null
$converted++
}
$conn.Close()
Write-Host "$converted 件のレコードを変換しました。" -ForegroundColor Green# 使い方
./Convert-MarkdownToHtml.ps1 -ServerInstance "localhost" -Database "Implem.Pleasanter" `
-TableName "Issues" -ColumnName "DescriptionA"変換後の確認
ステップ 1 の調査クエリを再実行し、対象のカラムの
FieldCssが"field-rte"になっていることを確認します。[md]が残っているレコードがないか、Description と Body の両方を確認します(PostgreSQL の例。SQL Server や MySQL でもLEFT関数で同様に確認できます)。sql-- Description カラムの残存確認 SELECT "SiteId", "IssueId", LEFT("DescriptionA", 20) AS "DescriptionA_preview" FROM "Issues" WHERE LEFT("DescriptionA", 4) = '[md]';プリザンターを再起動します。
SiteSettingsはアプリケーション内でキャッシュされているため、SQL で直接変更した後は再起動が必要です。IIS の場合はアプリケーションプールのリサイクル、Docker の場合はコンテナの再起動で対応できます。対象サイトの編集画面で、説明項目がリッチテキストエディタで表示されること、既存データが正しく表示されること、新規入力・保存・表示ができることを確認します。