diff --git a/AGENTS.md b/AGENTS.md index e5b9b7d..93f3d30 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -140,10 +140,22 @@ npm run preview 99 文字を超えるなら block 形式へ切り替へるか、message 定数化などで縮める。 - Ruby の method chain や call argument を折り返す際、call-site の `)` を block close のやうに独立させない。 +- Ruby では、行末の `\` を用途を問はず一切使用しない。 +- Ruby では、文字列連結、logger message、method call、条件式、SQL 断片、 + 正規表現その他すべての式で、行末バックスラッシュによる継続を禁止する。 - Ruby の block body は、その基準位置から 2 空白深くする。 - Ruby の wrapped expression、method argument、array element、hash pair などの continuation indentation は、その statement の基準位置から 4 空白深くする。 - Ruby の continuation indentation を、行頭からの絶対空白数として扱はない。 +- Ruby では、暗黙的に継続可能な構文を優先し、method call、array、Hash 及び + 括弧内ではバックスラッシュなしで改行する。 +- Ruby では、行長制限を守るために行末バックスラッシュを導入してはならない。 +- Ruby で行末バックスラッシュが必要に見える場合は、括弧内で自然に改行する、 + 一つの文字列補間へまとめる、中間変数へ分ける、`format` を使ふ、heredoc を + 使ふ、array 又は Hash を組み立ててから処理する、条件式全体を括弧で囲む、 + method へ抽出する、のいづれかへ書き換へる。 +- formatter 又は自動修正にも、Ruby の行末バックスラッシュを生成させない。 +- 新規 code だけでなく、今回触れる Ruby code にも行末バックスラッシュを残さない。 - 例へば class body 内の array は、class body の 2 空白を基準に、更に 4 空白 深くするため、結果として行頭から 6 空白になる。 @@ -197,6 +209,48 @@ end Bad: +```rb +Rails.logger.info( + "post_import_metadata_fetch_failure "\ + "#{ payload.to_json }") +``` + +Good: + +```rb +payload = { + error: e.class.name, + message: e.message } + +Rails.logger.info( + "post_import_metadata_fetch_failure #{ payload.to_json }") +``` + +Bad: + +```rb +message = "first "\ + "second" +``` + +Good: + +```rb +message = format( + '%s %s', + first: 'first', + second: 'second') +``` + +Bad: + +```rb +result = first_value + \ + second_value +``` + +Bad: + ```rb records.each { do_work(_1) } diff --git a/backend/app/models/post.rb b/backend/app/models/post.rb index 69f7e81..4f5b09d 100644 --- a/backend/app/models/post.rb +++ b/backend/app/models/post.rb @@ -138,8 +138,10 @@ class Post < ApplicationRecord end def self.section_literal section - "[#{ Post.ms_to_time(section.begin_ms) }-"\ - "#{ section.end_ms ? Post.ms_to_time(section.end_ms) : '' }]" + end_ms = + section.end_ms ? Post.ms_to_time(section.end_ms) : '' + + "[#{ Post.ms_to_time(section.begin_ms) }-#{ end_ms }]" end def self.ms_to_time ms diff --git a/backend/app/representations/post_repr.rb b/backend/app/representations/post_repr.rb index 1590ce0..0dc8a9b 100644 --- a/backend/app/representations/post_repr.rb +++ b/backend/app/representations/post_repr.rb @@ -106,11 +106,14 @@ module PostRepr Rails.application.routes.url_helpers.rails_storage_proxy_url(post.thumbnail, **options) rescue ActionController::UrlGenerationError, ArgumentError, URI::InvalidURIError => e - Rails.logger.warn( - "PostRepr.thumbnail_url failed post_id=#{post.id} " \ - "attachment_id=#{post.thumbnail.attachment&.id} " \ - "blob_id=#{post.thumbnail.blob&.id} " \ - "error_class=#{e.class} message=#{e.message}") + payload = { + post_id: post.id, + attachment_id: post.thumbnail.attachment&.id, + blob_id: post.thumbnail.blob&.id, + error_class: e.class, + message: e.message } + + Rails.logger.warn("PostRepr.thumbnail_url failed #{ payload.to_json }") nil end end diff --git a/backend/app/services/post_import_previewer.rb b/backend/app/services/post_import_previewer.rb index 96cfdbe..0a812f5 100644 --- a/backend/app/services/post_import_previewer.rb +++ b/backend/app/services/post_import_previewer.rb @@ -225,15 +225,15 @@ class PostImportPreviewer end { data:, warnings:, validation_errors: { } } rescue Preview::UrlSafety::UnsafeUrl => e + payload = { error: e.class.name, message: e.message } Rails.logger.info( - "post_import_metadata_fetch_unsafe_url "\ - "#{ { error: e.class.name, message: e.message }.to_json }") + "post_import_metadata_fetch_unsafe_url #{ payload.to_json }") { data: { }, warnings: { }, validation_errors: { url: [e.message] } } rescue Preview::HttpFetcher::FetchFailed, Preview::HttpFetcher::ResponseTooLarge => e + payload = { error: e.class.name, message: e.message } Rails.logger.info( - "post_import_metadata_fetch_failure "\ - "#{ { error: e.class.name, message: e.message }.to_json }") + "post_import_metadata_fetch_failure #{ payload.to_json }") { data: { }, warnings: { 'url' => [METADATA_FETCH_WARNING] }, validation_errors: { } } end @@ -335,14 +335,14 @@ class PostImportPreviewer def safe_fetch_metadata url fetch_metadata(url) rescue Preview::UrlSafety::UnsafeUrl => e + payload = { error: e.class.name, message: e.message } Rails.logger.info( - "post_import_metadata_fetch_unsafe_url "\ - "#{ { error: e.class.name, message: e.message }.to_json }") + "post_import_metadata_fetch_unsafe_url #{ payload.to_json }") { data: { }, warnings: { }, validation_errors: { url: [e.message] } } rescue StandardError => e + payload = { error: e.class.name, message: e.message } Rails.logger.error( - "post_import_metadata_fetch_unexpected_failure "\ - "#{ { error: e.class.name, message: e.message }.to_json }") + "post_import_metadata_fetch_unexpected_failure #{ payload.to_json }") { data: { }, warnings: { 'url' => [METADATA_FETCH_WARNING] }, validation_errors: { } } end diff --git a/backend/app/services/post_metadata_fetcher.rb b/backend/app/services/post_metadata_fetcher.rb index 33fc3d9..a8dcb6d 100644 --- a/backend/app/services/post_metadata_fetcher.rb +++ b/backend/app/services/post_metadata_fetcher.rb @@ -3,10 +3,9 @@ require 'date' class PostMetadataFetcher TIMESTAMP_PATTERN = - Regexp.new( - '\A(\d{4})-(\d{2})-(\d{2})T(\d{2})' \ - '(?::(\d{2})(?::(\d{2})(?:\.(\d+))?)?)?' \ - '(Z|[+-]\d{2}:?\d{2})?\z') + /\A(\d{4})-(\d{2})-(\d{2})T(\d{2}) + (?::(\d{2})(?::(\d{2})(?:\.(\d+))?)?)? + (Z|[+-]\d{2}:?\d{2})?\z/x def self.fetch raw_url uri, = Preview::UrlSafety.validate(raw_url) diff --git a/backend/config/environments/development.rb b/backend/config/environments/development.rb index 365b5c0..8890673 100644 --- a/backend/config/environments/development.rb +++ b/backend/config/environments/development.rb @@ -1,4 +1,4 @@ -require "active_support/core_ext/integer/time" +require 'active_support/core_ext/integer/time' Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -17,8 +17,8 @@ Rails.application.configure do # Enable/disable Action Controller caching. By default Action Controller caching is disabled. # Run rails dev:cache to toggle Action Controller caching. - if Rails.root.join("tmp/caching-dev.txt").exist? - config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" } + if Rails.root.join('tmp/caching-dev.txt').exist? + config.public_file_server.headers = { 'cache-control' => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false end diff --git a/backend/spec/requests/posts_spec.rb b/backend/spec/requests/posts_spec.rb index ef98148..5007fb9 100644 --- a/backend/spec/requests/posts_spec.rb +++ b/backend/spec/requests/posts_spec.rb @@ -101,34 +101,34 @@ RSpec.describe 'Posts API', type: :request do end end - describe "GET /posts" do + describe 'GET /posts' do let!(:user) { create_member_user! } - let!(:tag_name) { TagName.create!(name: "spec_tag") } + let!(:tag_name) { TagName.create!(name: 'spec_tag') } let!(:tag) { Tag.create!(tag_name:, category: :general) } let!(:tag_name2) { TagName.create!(name: 'unko') } let!(:tag2) { Tag.create!(tag_name: tag_name2, category: :deerjikist) } let!(:alias_tag_name) { TagName.create!(name: 'manko', canonical: tag_name) } let!(:hit_post) do - Post.create!(uploaded_user: user, title: "hello spec world", + Post.create!(uploaded_user: user, title: 'hello spec world', url: 'https://example.com/spec2').tap do |p| PostTag.create!(post: p, tag:) end end let!(:miss_post) do - Post.create!(uploaded_user: user, title: "unrelated title", + Post.create!(uploaded_user: user, title: 'unrelated title', url: 'https://example.com/spec3').tap do |p| PostTag.create!(post: p, tag: tag2) end end it 'returns posts with tag name in JSON' do - get "/posts" + get '/posts' expect(response).to have_http_status(:ok) - posts = json.fetch("posts") + posts = json.fetch('posts') # 全postの全tagが name を含むこと expect(posts).not_to be_empty @@ -141,8 +141,8 @@ RSpec.describe 'Posts API', type: :request do expect(json['count']).to be_an(Integer) # spec_tag を含む投稿が存在すること - all_tag_names = posts.flat_map { |p| p["tags"].map { |t| t["name"] } } - expect(all_tag_names).to include("spec_tag") + all_tag_names = posts.flat_map { |p| p['tags'].map { |t| t['name'] } } + expect(all_tag_names).to include('spec_tag') end it 'keeps children and sections keys in non-detail tag responses' do @@ -201,10 +201,10 @@ RSpec.describe 'Posts API', type: :request do end it 'returns empty posts when nothing matches' do - get "/posts", params: { tags: "no_such_keyword_12345" } + get '/posts', params: { tags: 'no_such_keyword_12345' } expect(response).to have_http_status(:ok) - expect(json.fetch("posts")).to eq([]) + expect(json.fetch('posts')).to eq([]) expect(json.fetch('count')).to eq(0) end end @@ -1712,8 +1712,10 @@ RSpec.describe 'Posts API', type: :request do expect(post_record.reload.title).to eq('updated by other user') end - it 'returns 409 with mergeable true when stale tag changes'\ - ' do not conflict but merge is not requested' do + it( + 'returns 409 with mergeable true when stale tag changes ' + 'do not conflict but merge is not requested' + ) do sign_in_as(member) base_version = create_post_version_for!(post_record.reload) diff --git a/backend/spec/tasks/nico_sync_spec.rb b/backend/spec/tasks/nico_sync_spec.rb index 88f507d..75e531f 100644 --- a/backend/spec/tasks/nico_sync_spec.rb +++ b/backend/spec/tasks/nico_sync_spec.rb @@ -1,10 +1,10 @@ -require "rails_helper" +require 'rails_helper' -RSpec.describe "nico:sync" do +RSpec.describe 'nico:sync' do def stub_python(json_array) status = instance_double(Process::Status, success?: true) - allow(Open3).to receive(:capture3).and_return([json_array.to_json, "", status]) + allow(Open3).to receive(:capture3).and_return([json_array.to_json, '', status]) end def create_tag!(name, category:) @@ -25,12 +25,12 @@ RSpec.describe "nico:sync" do ) # 既存の非nicoタグ(kept_non_nico_ids) - kept_general = create_tag!("spec_kept", category: "general") + kept_general = create_tag!('spec_kept', category: 'general') PostTag.create!(post: post, tag: kept_general) # 追加される linked tag を準備(nico tag に紐付く一般タグ) - linked = create_tag!("spec_linked", category: "general") - nico = create_tag!("nico:AAA", category: "nico") + linked = create_tag!('spec_linked', category: 'general') + nico = create_tag!('nico:AAA', category: 'nico') link_nico_to_tag!(nico, linked) # bot / tagme は task 内で使うので作っておく(Tag.bot/tagme がある前提) @@ -46,22 +46,22 @@ RSpec.describe "nico:sync" do 'deleted_at' => '2026-01-31 00:00:00' }]) # 外部HTTPは今回「既存 post なので呼ばれない」はずだが、念のため塞ぐ - allow(URI).to receive(:open).and_return(StringIO.new("")) + allow(URI).to receive(:open).and_return(StringIO.new('')) - run_rake_task("nico:sync") + run_rake_task('nico:sync') post.reload - active_tag_names = post.tags.joins(:tag_name).pluck("tag_names.name") + active_tag_names = post.tags.joins(:tag_name).pluck('tag_names.name') - expect(active_tag_names).to include("spec_kept") - expect(active_tag_names).to include("nico:AAA") - expect(active_tag_names).to include("spec_linked") + expect(active_tag_names).to include('spec_kept') + expect(active_tag_names).to include('nico:AAA') + expect(active_tag_names).to include('spec_linked') expect(post.original_created_from).to eq(Time.iso8601('2026-01-01T03:34:00Z')) expect(post.original_created_before).to eq(Time.iso8601('2026-01-01T03:35:00Z')) # 差分が出るので bot が付く(kept_non_nico_ids != desired_non_nico_ids) - expect(active_tag_names).to include("bot操作") + expect(active_tag_names).to include('bot操作') end it '既存 post のサムネール取得に共通 attach 経路を使ふ' do @@ -116,21 +116,21 @@ RSpec.describe "nico:sync" do ) # 旧nicoタグ(今回の同期結果に含まれない) - old_nico = create_tag!("nico:OLD", category: "nico") + old_nico = create_tag!('nico:OLD', category: 'nico') old_pt = PostTag.create!(post: post, tag: old_nico) expect(old_pt.discarded_at).to be_nil # 今回は NEW のみ欲しい - new_nico = create_tag!("nico:NEW", category: "nico") + new_nico = create_tag!('nico:NEW', category: 'nico') # bot/tagme 念のため Tag.bot Tag.tagme - stub_python([{ "code" => "sm9", "title" => "t", "tags" => ["NEW"] }]) - allow(URI).to receive(:open).and_return(StringIO.new("")) + stub_python([{ 'code' => 'sm9', 'title' => 't', 'tags' => ['NEW'] }]) + allow(URI).to receive(:open).and_return(StringIO.new('')) - run_rake_task("nico:sync") + run_rake_task('nico:sync') # OLD は active から外れる(discarded_at が入る) old_pts = PostTag.where(post_id: post.id, tag_id: old_nico.id).order(:id).to_a @@ -138,9 +138,9 @@ RSpec.describe "nico:sync" do # NEW は active にいる post.reload - active_names = post.tags.joins(:tag_name).pluck("tag_names.name") - expect(active_names).to include("nico:NEW") - expect(active_names).not_to include("nico:OLD") + active_names = post.tags.joins(:tag_name).pluck('tag_names.name') + expect(active_names).to include('nico:NEW') + expect(active_names).not_to include('nico:OLD') end def snapshot_tags(post) diff --git a/frontend/src/pages/materials/MaterialListPage.tsx b/frontend/src/pages/materials/MaterialListPage.tsx index ea7dbb7..7a5b718 100644 --- a/frontend/src/pages/materials/MaterialListPage.tsx +++ b/frontend/src/pages/materials/MaterialListPage.tsx @@ -13,7 +13,7 @@ import MainArea from '@/components/layout/MainArea' import { SITE_TITLE } from '@/config' import { fetchMaterials, parseMaterialFilter } from '@/lib/materials' import { materialsKeys } from '@/lib/queryKeys' -import { dateString, inputClass } from '@/lib/utils' +import { cn, dateString, inputClass } from '@/lib/utils' import type { FC, FormEvent } from 'react' @@ -113,10 +113,11 @@ const clearedTagSelectionPath = ( const MaterialThumb: FC<{ material: Material }> = ({ material }) => (
+ className={cn ( + 'flex aspect-square h-[180px] w-[180px] items-center justify-center', + 'overflow-hidden rounded-lg border border-stone-200 bg-white text-center', + 'text-stone-900 shadow-sm dark:border-stone-700 dark:bg-stone-900', + 'dark:text-stone-100')}> {material.thumbnail ? : ( @@ -531,30 +532,32 @@ const MaterialListPage: FC = () => {
- -