Redmine 6.0で使えるようになる「部分引用機能」の紹介とその実装


My Redmine

こんにちは、日高です。早いもので、入社7ヶ月が経過しました。 今回は、私が実装しRedmine 6.0で使えるようになる「部分引用機能」をご紹介します。また、どのように実現されているかについても簡単に説明します。

Redmine 5.1 までの引用機能と課題

Redmineには、チケットやフォーラムのメッセージを引用して返信する機能があります。メッセージ欄の引用ボタンをクリックすると、そのメッセージ全体が返信欄に引用文としてセットされます。


メッセージ欄にある引用ボタン

しかし、メッセージの一部を引用する場合は、一度全体を引用してから不要な内容を削除するか、引用したい内容を手動でコピーして返信欄に貼り付けるという手間をかける必要がありました。

今回ご紹介する「部分引用機能」は、これを解決する新しい機能です。

Redmine 6.0 から使える部分引用機能

Feature #41294 Partial quoting feature for Issues and Forums

主な特徴は次のとおりです。

選択した内容のみ引用する

引用したい内容を選択して引用ボタンをクリックすると、その内容のみ引用されます。何も選択していない状態でクリックした場合は、従来通りメッセージ全体が引用されます。


引用する内容を選択して引用ボタンをクリック

選択した内容のみ引用される

引用元のスタイルを維持して引用する

CommonMark Markdownを使用している場合は、選択した内容のスタイル(太字やリスト、コードハイライトなど)も維持されます。


引用する内容を選択して引用ボタンをクリック

CommonMark Markdown を使っている場合は引用元のスタイルも維持

なお、ご覧の通りテーブルのスタイルは現時点では維持されません。テーブルの内容がテキストとして引用されます。

また、#1234のような Redmine 固有のリンクは、通常のMarkdown形式のリンクとして引用されます。 例えば、#1234を引用すると[#1234](/issues/1234)となります。

一方、CommonMark Markdown 以外を使用している場合は、選択内容は常にテキストとして引用されます。スタイルは維持されません。


引用する内容を選択して引用ボタンをクリック

CommonMark Markdown 以外を使っている場合はテキストとして引用

どのように実現したか

この機能を実現するにあたっては、主に次の二点を考える必要がありました。

  1. 選択範囲の取得と制御
  2. 選択範囲の Markdown(CommonMark)スタイルの取得

選択範囲の取得と制御

まず、選択されている内容を取得しなければ始まりません。具体的には次のことを行う必要があります。

これらについては、主にSelection APIRangeを使うことで実現できます。

対象のメッセージが選択されているかを判定する


対象のメッセージとは、上記のオレンジの四角で囲まれた内容を指します。この内容が選択範囲に含まれているかを調べる必要があります。 これは、Selection.containsNodeを使って簡単に判定できます。

quote_reply.jsより:

get isSelected() {
  return this.selection.containsNode(this.targetElement, true);
}

二つ目の引数のpartialContainmenttrueにする必要があります。

When true, containsNode() returns true 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.setStartBeforeSelection.setEndAfterを、 対象のメッセージが選択範囲の全部または一部が含まれているかの判定には、Range.startContainerRange.endContainerを使うことで実現できます。

quote_reply.jsより:

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)スタイルの取得

ようやく選択範囲を取得できましたが、最終的に必要なのは選択範囲のMarkdown(CommonMark)スタイルです。

選択範囲からHTMLを取得して、TurndownによってHTMLからMarkdown(CommonMark)に変換することで実現します。

基本的にはrangeからHTMLを取り出してTurndownに渡すだけですが、HTMLの加工やTurndownへの設定によって、Redmineに合うようにいくつかの調整を行っています。

例えば、コードハイライトの中身を引用する場合に、適切にハイライトされるような対処を行っていたりします。以下のようなケースです。


コードブロック内のコードを選択して引用ボタンをクリック

正しくコードハイライトされる

このケースでは、取得できる範囲rangeにはテキストしか含まれないため、正しいMarkdownに変換するために、予め適切なHTML構造を構築しておく必要があります。 詳細は以下のコードのQuoteCommonMarkFormatterクラスを参照してください。

quote_reply.jsより:

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フレームワークなどを使用したアプリケーション開発を行うスタッフを募集しています。採用情報の詳細 弊社での勤務に関心をお持ちの方は、知り合いの弊社社員・関係者を通じてご連絡ください。

My Redmine

こちらの記事もオススメです!
Redmineとプラグインの開発を支える自作のツール
Redmineとプラグインの開発をしやすくするために作ったツールを二つ紹介。
来るRedmine 6.0でのデフォルトテーマの改善状況:見やすく、読みやすく
来るRedmine 6.0ではデフォルトテーマが改善され見やすく、読みやすくなります。その改善内容を紹介します。
RedmineプラグインのシステムテストをPlaywrightで動かす
RedmineプラグインのシステムテストをPlaywrightで動かす方法を紹介します。
Turbo FramesでRedmineのフォーラム機能の画面遷移を削減できるか試した話
Redmineのフォーラム機能におけるユーザー体験を向上させるため、画面遷移を減らし、部分的にシームレスな更新を実現するためにTurbo Framesを試しました。
My Redmine Gen.2 のサービス運用面での改善点
My Redmine Gen.2 のコスト削減とパフォーマンス向上のために、S3のキャッシュ化、Graviton2対応、Pipelineの最適化を進めています。
ファーエンドテクノロジーからのお知らせ(2025/04/23更新)
社員研修に伴うサポート体制変更・休業のお知らせ(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」配信中
新バージョンやセキュリティ修正のリリース情報、そのほか最新情報を迅速にお届け