Skip to content

イベント説明文と開催レポートにマークダウン対応を追加#303

Merged
sugiwe merged 4 commits intomainfrom
feature/markdown-support
Mar 18, 2026
Merged

イベント説明文と開催レポートにマークダウン対応を追加#303
sugiwe merged 4 commits intomainfrom
feature/markdown-support

Conversation

@sugiwe
Copy link
Contributor

@sugiwe sugiwe commented Mar 18, 2026

概要

イベント説明文と開催レポートでマークダウン記法が使えるようになりました。

実装内容

1. Markdown対応

  • redcarpet gemを使用してMarkdownパース機能を実装
  • 標準的なMarkdown記法(見出し、太字、斜体、リスト、リンク、テーブル、コードブロックなど)に対応
  • XSS対策として厳格なサニタイズを実装

2. カスタムボタンリンク記法

チャットへのリンクなどを目立たせるため、独自のボタンリンク記法を追加:

<button-link url="https://slack.com/channels/xxx">Slackに参加</button-link>

青いボタンスタイルで表示され、target="_blank"rel="noopener noreferrer"が自動付与されます。

3. プレビュー機能

  • 「編集」「プレビュー」のタブ切り替えUI
  • リアルタイムプレビュー(APIエンドポイント経由)
  • Stimulusコントローラーによるタブ制御

4. ドキュメント

  • ヘルプページにマークダウン記法ガイドを追加
  • 各記法の使い方とサンプルを掲載

5. テスト

  • MarkdownHelperの単体テスト(14例、全てパス)
  • システムテスト(マークダウンエディタのUI)

変更ファイル

  • Gemfile - redcarpet gem追加
  • app/helpers/markdown_helper.rb - マークダウンレンダリング
  • app/controllers/markdown_previews_controller.rb - プレビューAPI
  • app/javascript/controllers/markdown_editor_controller.js - タブ切り替え
  • app/views/shared/_markdown_editor.html.erb - エディタコンポーネント
  • app/views/teams/events/_form.html.erb - フォーム更新
  • app/views/teams/events/_main_column.html.erb - 表示更新
  • app/views/home/help.html.erb - ヘルプページ更新
  • config/routes.rb - プレビューAPIルート追加

テスト計画

  • ヘルパーの単体テスト
  • マークダウンレンダリングの動作確認
  • XSS対策の確認
  • 実際のイベント作成・編集画面での動作確認
  • プレビュー機能の動作確認
  • ボタンリンクの表示確認

🤖 Generated with Claude Code

- redcarpet gemを追加してMarkdownパースを実装
- カスタムボタンリンク記法を含むMarkdownHelperを実装
- マークダウンプレビュー用APIエンドポイントを作成
- 編集/プレビュータブ切り替え用のStimulusコントローラーを追加
- イベントフォームをマークダウンエディタコンポーネントに更新
- ヘルプページにマークダウン記法ガイドを追加
- マークダウンレンダリングの包括的なテストを追加

機能:
- 標準マークダウン: 見出し、太字、斜体、リスト、リンク、テーブル、コード
- カスタムボタンリンク記法: <button-link url="...">ラベル</button-link>
- サニタイズによるXSS対策
- イベント作成・編集でのリアルタイムプレビュー

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

このプルリクエストは、イベントの説明文と開催レポートの入力フィールドにMarkdown記法を導入し、コンテンツの表現力を向上させることを目的としています。標準的なMarkdown記法に加え、特定のURLを目立たせるためのカスタムボタンリンク記法も利用可能です。ユーザーは編集中の内容をリアルタイムで確認できるプレビュー機能を通じて、より直感的にコンテンツを作成できます。また、セキュリティ対策として厳格なサニタイズ処理が施されており、ヘルプページには新しい記法のガイドが追加されています。

Highlights

  • Markdown記法の導入: イベント説明文と開催レポートの入力フィールドでMarkdown記法が利用可能になりました。
  • カスタムボタンリンク記法: 特定のURLを目立たせるための独自のボタンリンク記法(タグ)が追加されました。
  • リアルタイムプレビュー機能: Markdownエディタに「編集」と「プレビュー」のタブ切り替えUIと、リアルタイムプレビュー機能が実装されました。
  • セキュリティ強化: XSS対策として、厳格なサニタイズ処理がMarkdownレンダリングに導入されました。
  • ドキュメントとテスト: Markdown記法の利用ガイドがヘルプページに追加され、ヘルパーの単体テストとエディタのシステムテストが追加されました。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

このプルリクエストは、イベント説明文と開催レポートにMarkdownサポートを追加するもので、素晴らしい機能改善ですね。redcarpet gemの導入、プレビュー機能、カスタムの<button-link>記法など、実装は全体的に堅牢です。特に、サーバーサイドでのプレビュー生成とサニタイズ、そしてそれに対応する単体テストとシステムテストが追加されている点は高く評価できます。

いくつかの改善点を提案しました。特に、セキュリティに関するgsubでのスクリプトタグ除去は、より堅牢なSafeListSanitizerに一本化することを強く推奨します。また、システムテストの安定性を向上させるための修正も提案しています。

これらの点を修正いただくことで、より安全で信頼性の高い機能になると思います。

Comment on lines +86 to +87
# First remove script tags completely
html = html.gsub(/<script\b[^>]*>.*?<\/script>/im, '')

Choose a reason for hiding this comment

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

high

gsub を使った <script> タグの手動削除は、堅牢ではなく、バイパスされる可能性があります。例えば、<SCRipt> のような大文字小文字の違いや、onerror 属性を持つ他のタグは捕捉できません。90行目から使用している Rails::HTML5::SafeListSanitizer が、より安全で包括的なXSS対策を提供します。このサニタイザは、許可されていないタグ(<script> を含む)や属性をすべて削除するため、この gsub は不要であり、かえってセキュリティ対策の意図を曖昧にする可能性があります。サニタイザに一本化することをお勧めします。

Comment on lines +39 to +43
# プレビューが表示される(非同期処理なので少し待つ)
sleep 1

# 編集エリアが隠れている
expect(page).not_to have_field('イベント説明', visible: true)

Choose a reason for hiding this comment

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

medium

sleep 1 の使用は、テストの実行環境によって挙動が変わり、テストが不安定になる(flaky test)原因となります。また、このテストではプレビューが正しく表示されたことの確認が抜けています。
Capybaraの待機機能を利用して、プレビュー内容がレンダリングされたことを確認することで、sleepを削除し、より堅牢なテストにすることができます。

      # プレビューが表示され、編集エリアが隠れていることを確認
      within(find('[data-markdown-editor-target="previewPane"]')) do
        expect(page).to have_selector('strong', text: 'Bold text')
        expect(page).to have_selector('em', text: 'italic text')
      end
      expect(page).not_to have_field('イベント説明', visible: true)

sugiwe and others added 2 commits March 18, 2026 17:21
- JavaScript: Prettierでフォーマット修正
- Ruby: RuboCopでスタイル違反を修正(文字列リテラル、Capybaマッチャー)
- テスト: 曖昧なボタンマッチングとvisibility判定を修正

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Geminiのコードレビューフィードバックに基づく改善:

- 冗長なscriptタグのgsub処理を削除
  SafeListSanitizerが既に適切に処理するため不要
- sanitizationテストの期待値を修正
  scriptタグの削除のみを検証するように変更

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@sugiwe
Copy link
Contributor Author

sugiwe commented Mar 18, 2026

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

このプルリクエストは、イベント説明文と開催レポートにMarkdownサポートを追加するもので、素晴らしい機能追加です。redcarpet gemの導入、カスタムの<button-link>記法、リアルタイムプレビュー機能など、実装は全体的に良くできています。

いくつかの改善点を提案させていただきました。

  • MarkdownHelper の堅牢性を向上させるための正規表現とプレースホルダー生成方法の修正。
  • MarkdownPreviewsController のロジックの簡素化。
  • システムテストの安定性を高めるための sleep の削除。

これらの修正により、コードの品質と信頼性がさらに向上するかと思います。

# カスタムボタンタグを抽出してプレースホルダーに置き換え
def extract_button_links(text)
buttons = []
text_with_placeholders = text.gsub(/<button-link\s+url="([^"]+)">([^<]+)<\/button-link>/m) do

Choose a reason for hiding this comment

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

high

現在の正規表現 /<button-link\s+url="([^"]+)">([^<]+)<\/button-link>/m は、ボタンのラベルに < を含めることができず、堅牢性に欠けます。例えば、1 < 2 のようなテキストは使用できません。

より柔軟な非貪欲マッチ (.*?) を使用することをお勧めします。

/<button-link\s+url="([^"]+)">(.*?)<\/button-link>/m

これにより、ラベル内のテキストが link_to ヘルパーによって安全にエスケープされるようになります。

    text_with_placeholders = text.gsub(/<button-link\s+url="([^"]+)">(.*?)<\/button-link>/m) do


buttons << { url: url, label: label }
# Use a placeholder that won't be affected by markdown parsing
"BUTTONPLACEHOLDER#{buttons.size - 1}ENDPLACEHOLDER"

Choose a reason for hiding this comment

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

high

"BUTTONPLACEHOLDER#{buttons.size - 1}ENDPLACEHOLDER" のような連番ベースのプレースホルダーは、ユーザーが同じ文字列を入力した場合に予期せぬ置換を引き起こす可能性があります。

SecureRandom.hex を使用して一意なプレースホルダーを生成する方がはるかに安全です。

placeholder = "BUTTONPLACEHOLDER-#{SecureRandom.hex(8)}"
buttons << { url: url, label: label, placeholder: placeholder }
placeholder

この変更を適用するには、restore_button_placeholders メソッドも更新して、インデックスではなく保存されたプレースホルダーを使用するように修正する必要があります。

Comment on lines +39 to +40
# プレビューが表示される(非同期処理なので少し待つ)
sleep 1

Choose a reason for hiding this comment

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

high

システムテストで sleep を使用すると、テストが不安定になる(flaky test)可能性があります。特定の要素が表示されるのを待つなど、より堅牢な方法に修正することをお勧めします。この場合は、プレビューコンテンツがレンダリングされるのを待つことができます。

      # プレビューが表示されるのを待つ
      within(first('[data-markdown-editor-target="preview"]')) do
        expect(page).to have_css('strong', text: 'Bold text')
        expect(page).to have_css('em', text: 'italic text')
      end

Comment on lines +9 to +20
def create
text = params[:text]

if text.blank?
render json: { html: '' }
return
end

html = render_markdown(text)

render json: { html: html }
end

Choose a reason for hiding this comment

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

medium

このメソッドは簡潔にできます。render_markdown ヘルパーは既に空のテキスト入力を処理して空文字列を返すため、コントローラーでの明示的なチェックは不要です。

  def create
    html = render_markdown(params[:text])

    render json: { html: html }
  end

以下の改善を実施:

1. プレースホルダー生成をSecureRandomベースに変更
   - 連番ベースからSecureRandom.hexに変更
   - ユーザー入力との衝突を防ぐため、より安全な実装に
   - restore_button_placeholdersメソッドも更新

2. MarkdownPreviewsControllerを簡潔化
   - render_markdownが既に空文字列処理をしているため、
     コントローラーでの冗長なblank?チェックを削除
   - コードがより簡潔でメンテナンス性が向上

正規表現の変更については、セキュリティの観点から現状維持とした。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@sugiwe sugiwe self-assigned this Mar 18, 2026
@sugiwe
Copy link
Contributor Author

sugiwe commented Mar 18, 2026

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

イベント説明と開催レポートにマークダウン機能を導入する素晴らしいプルリクエストですね。redcarpetの導入、プレビュー機能、カスタムの<button-link>記法など、機能が充実しており、ユーザーにとって非常に便利になると思います。特に、サーバーサイドでの厳格なサニタイズや、XSS対策を考慮した設計は高く評価できます。テストも単体テストとシステムテストの両方が追加されており、品質の高い変更だと感じました。1点、MarkdownHelperの実装について、より堅牢性を高めるための改善提案をコメントしましたので、ご確認いただければ幸いです。全体として、非常によくできた実装です。

next '' unless url.match?(%r{\Ahttps?://})

# 一意なプレースホルダーを生成(ユーザー入力との衝突を防ぐ)
placeholder = "BUTTONPLACEHOLDER-#{SecureRandom.hex(8)}"

Choose a reason for hiding this comment

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

medium

現在のプレースホルダーの実装では、ユーザーが BUTTONPLACEHOLDER-xxxx のような文字列を意図的に、あるいは偶然入力した場合に、意図しないボタンに置換されてしまうわずかな可能性があります。

このリスクを軽減するために、プレースホルダーをHTMLコメントで囲むことをお勧めします。ユーザーがHTMLコメントを直接入力する可能性は低く、より安全になります。また、もし入力されたとしてもHTMLコメントはレンダリングされないため、問題が発生しにくいです。

      placeholder = "<!-- BUTTONPLACEHOLDER-#{SecureRandom.hex(8)} -->"

@sugiwe sugiwe merged commit ce22a26 into main Mar 18, 2026
5 checks passed
@sugiwe sugiwe deleted the feature/markdown-support branch March 18, 2026 12:38
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.

1 participant