こんにちは、日高です。早いもので、入社7ヶ月が経過しました。 今回は、私が実装しRedmine 6.0で使えるようになる「部分引用機能」をご紹介します。また、どのように実現されているかについても簡単に説明します。
Redmineには、チケットやフォーラムのメッセージを引用して返信する機能があります。メッセージ欄の引用ボタンをクリックすると、そのメッセージ全体が返信欄に引用文としてセットされます。
しかし、メッセージの一部を引用する場合は、一度全体を引用してから不要な内容を削除するか、引用したい内容を手動でコピーして返信欄に貼り付けるという手間をかける必要がありました。
今回ご紹介する「部分引用機能」は、これを解決する新しい機能です。
Feature #41294 Partial quoting feature for Issues and Forums
主な特徴は次のとおりです。
引用したい内容を選択して引用ボタンをクリックすると、その内容のみ引用されます。何も選択していない状態でクリックした場合は、従来通りメッセージ全体が引用されます。
CommonMark Markdownを使用している場合は、選択した内容のスタイル(太字やリスト、コードハイライトなど)も維持されます。
なお、ご覧の通りテーブルのスタイルは現時点では維持されません。テーブルの内容がテキストとして引用されます。
また、#1234
のような Redmine 固有のリンクは、通常のMarkdown形式のリンクとして引用されます。
例えば、#1234
を引用すると[#1234](/issues/1234)
となります。
一方、CommonMark Markdown 以外を使用している場合は、選択内容は常にテキストとして引用されます。スタイルは維持されません。
この機能を実現するにあたっては、主に次の二点を考える必要がありました。
まず、選択されている内容を取得しなければ始まりません。具体的には次のことを行う必要があります。
これらについては、主にSelection APIとRangeを使うことで実現できます。
対象のメッセージとは、上記のオレンジの四角で囲まれた内容を指します。この内容が選択範囲に含まれているかを調べる必要があります。 これは、Selection.containsNodeを使って簡単に判定できます。
get isSelected() { return this.selection.containsNode(this.targetElement, true); }
二つ目の引数のpartialContainment
をtrue
にする必要があります。
When
true
,containsNode()
returnstrue
when a part of the node is part of the selection.
また、Selection API は複数の選択範囲をサポートしています。少なくとも、Firefox は複数選択をサポートしているため、このケースも考慮する必要があります。
これは、Selection.rangeCountで選択範囲の数を取得し、Selection.getRangeAtで各選択範囲を取得して、対象のメッセージが範囲に含まれているかを判定することで対処できます。
quote_reply.js より:
for (let i = 0; i < this.selection.rangeCount; i++) { let range = this.selection.getRangeAt(i); if (range.intersectsNode(this.targetElement)) { return range; } }
Range.intersectsNodeは、指定されたノードが範囲内に含まれているか(交差しているか)を判定します。 これを使って、対象のメッセージを含む最初の範囲を取得します。
対象のメッセージが選択範囲に全部または一部が含まれている場合、対象のメッセージ以外の選択範囲を除去する必要があります。
例えば、次のケースでは、オレンジの範囲前後の選択範囲を削除します。
範囲の加工には、Selection.setStartBeforeとSelection.setEndAfterを、 対象のメッセージが選択範囲の全部または一部が含まれているかの判定には、Range.startContainerとRange.endContainerを使うことで実現できます。
if (!this.targetElement.contains(range.startContainer)) { range.setStartBefore(this.targetElement); } if (!this.targetElement.contains(range.endContainer)) { range.setEndAfter(this.targetElement); }
range
は、前段でSelection.getRangeAt
で取得した選択範囲です。
Range.startContainer
によって、範囲の最初のノードを取得し、
そのノードが対象メッセージの要素内に無ければ、対象メッセージの要素の直前を範囲の開始位置に設定しています。範囲の終了位置の処理も同様に設定します。
ようやく選択範囲を取得できましたが、最終的に必要なのは選択範囲のMarkdown(CommonMark)スタイルです。
選択範囲からHTMLを取得して、TurndownによってHTMLからMarkdown(CommonMark)に変換することで実現します。
基本的にはrange
からHTMLを取り出してTurndownに渡すだけですが、HTMLの加工やTurndownへの設定によって、Redmineに合うようにいくつかの調整を行っています。
例えば、コードハイライトの中身を引用する場合に、適切にハイライトされるような対処を行っていたりします。以下のようなケースです。
このケースでは、取得できる範囲range
にはテキストしか含まれないため、正しいMarkdownに変換するために、予め適切なHTML構造を構築しておく必要があります。
詳細は以下のコードのQuoteCommonMarkFormatter
クラスを参照してください。
class QuoteCommonMarkFormatter { format(selectedRange) { if (!selectedRange) { return null; } const htmlFragment = this.extractHtmlFragmentFrom(selectedRange); const preparedHtml = this.prepareHtml(htmlFragment); return this.convertHtmlToCommonMark(preparedHtml); } ... }
今回ご紹介した「部分引用機能」は、Redmine 6.0に含まれる予定です。ぜひお試しください。
【スタッフ募集中】 弊社ではAWSを活用したソリューションの企画・設計・構築・運用や、Ruby on Rails・JavaScriptフレームワークなどを使用したアプリケーション開発を行うスタッフを募集しています。採用情報の詳細 弊社での勤務に関心をお持ちの方は、知り合いの弊社社員・関係者を通じてご連絡ください。
![]() |
Redmineとプラグインの開発をしやすくするために作ったツールを二つ紹介。 |
![]() |
来るRedmine 6.0ではデフォルトテーマが改善され見やすく、読みやすくなります。その改善内容を紹介します。 |
![]() |
RedmineプラグインのシステムテストをPlaywrightで動かす方法を紹介します。 |
![]() |
Redmineのフォーラム機能におけるユーザー体験を向上させるため、画面遷移を減らし、部分的にシームレスな更新を実現するためにTurbo Framesを試しました。 |
![]() |
My Redmine Gen.2 のコスト削減とパフォーマンス向上のために、S3のキャッシュ化、Graviton2対応、Pipelineの最適化を進めています。 |
![]() |
社員研修に伴うサポート体制変更・休業のお知らせ(5/20〜23) 社員研修に伴い、5月20日〜23日はサポート体制の変更および休業とさせていただきます。 |
![]() |
オープンソースカンファレンス2025 Nagoyaに弊社代表の前田が登壇(ブース出展あり) オープンソースカンファレンス(OSC)2025 Nagoyaに弊社代表の前田が登壇。『Redmineの意外と知らない便利な機能(Redmine 6.0 対応版)』をテーマに発表します。 |
![]() |
エンタープライズプラン向け「優先サポート」を開始 My Redmineでは、エンタープライズプランをご契約のお客様向けにサポート対応を優先的に行う「優先サポート(プライオリティサポート)」を開始いたしました。 |
![]() |
プロジェクト管理ツール「RedMica」バージョン 3.1.0をリリース Redmine互換のオープンソースソフトウェア ファーエンドテクノロジー株式会社は、2024年11月19日(日本時間)、Redmine互換のプロジェクト管理ソフトウェア「RedMica 3.1.0」をリリースしました。 |
![]() |
Redmineの最新情報をメールでお知らせする「Redmine News」配信中 新バージョンやセキュリティ修正のリリース情報、そのほか最新情報を迅速にお届け |