From a847d93d2f0fcdb6b7371a0923378834cc824555 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Sat, 18 Jul 2026 22:18:20 +0900 Subject: [PATCH] #399 --- AGENTS.md | 76 +++++-- backend/app/models/post.rb | 21 +- backend/app/services/post_create_plan.rb | 4 +- backend/spec/requests/posts_spec.rb | 21 +- backend/spec/tasks/nico_sync_spec.rb | 12 +- frontend/src/components/PostEditForm.test.tsx | 6 +- frontend/src/components/TagLink.tsx | 7 +- frontend/src/components/ui/dialog.tsx | 192 +++++++++--------- .../pages/theatres/TheatreDetailPage.test.tsx | 6 +- 9 files changed, 198 insertions(+), 147 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 876b13f..e5b9b7d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -124,7 +124,7 @@ npm run preview - For arrays, never put whitespace or a line break immediately before `]`. - Keep the first element on the same line as `[` by default. - If an array would exceed the line limit, break after `[` and indent - elements by 4 spaces. + elements 4 spaces deeper than the statement's base indentation. ### Ruby delimiter and wrapping rules @@ -140,6 +140,12 @@ npm run preview 99 文字を超えるなら block 形式へ切り替へるか、message 定数化などで縮める。 - Ruby の method chain や call argument を折り返す際、call-site の `)` を block close のやうに独立させない。 +- Ruby の block body は、その基準位置から 2 空白深くする。 +- Ruby の wrapped expression、method argument、array element、hash pair などの + continuation indentation は、その statement の基準位置から 4 空白深くする。 +- Ruby の continuation indentation を、行頭からの絶対空白数として扱はない。 +- 例へば class body 内の array は、class body の 2 空白を基準に、更に 4 空白 + 深くするため、結果として行頭から 6 空白になる。 Bad: @@ -225,20 +231,37 @@ records.each { formatting as the local reference shape: component and callback bodies use 2-space block indentation, single-line bodies do not gain unnecessary braces, wrapped expressions use 4-space continuation indentation, multi- - stage ternaries use explicit parentheses, and only complete leading runs - of 8 spaces are compressed to tabs. -- In TypeScript and TSX only, tabs are for leading 8-column compression only. + stage ternaries use explicit parentheses, and TypeScript / TSX indentation + and alignment whitespace compress every complete run of 8 spaces to tabs. - A tab does not represent one indentation level. -- Do not replace 2-space or 4-space indentation with tabs. -- First determine visible indentation using 2-space block indentation and - 4-space continuation indentation, then compress only complete leading runs - of 8 spaces into tabs. -- Tabs are only for leading indentation, never for spaces after non-space - text. -- Keep residual leading 2, 4, or 6 spaces after any tab compression. +- In TypeScript and TSX, first determine visible indentation using 2-space + block indentation and 4-space continuation indentation, then compress every + complete run of 8 spaces used for indentation or column alignment to tabs. +- In TypeScript and TSX, 8-space compression is mandatory, not optional. +- In TypeScript and TSX, this applies both to leading indentation and to + alignment whitespace after non-space text, such as aligned inline type + columns. +- In TypeScript and TSX, keep residual 2, 4, or 6 spaces after each tab + compression. +- In TypeScript and TSX, do not treat a tab as one logical indentation level. +- In TypeScript and TSX, do not alter string literals, template-literal + contents, regular expressions, or user-facing text merely to apply tab + compression. - Examples: 2 columns = 2 spaces, 4 columns = 4 spaces, 6 columns = 6 spaces, 8 columns = 1 tab, 10 columns = 1 tab + 2 spaces, 12 columns = 1 - tab + 4 spaces, 16 columns = 2 tabs. + tab + 4 spaces, 14 columns = 1 tab + 6 spaces, 16 columns = 2 tabs. +- When TypeScript or TSX code already uses column alignment, apply the same + 8-space compression rule to that alignment whitespace. +- Example: + +```ts +type Props = { + row: PostImportRow + displayNumber?: number + onEdit?: () => void + onRetry?: () => void + onToggleSkip?: (checked: boolean) => void } +``` - TypeScript and TSX imports may stay on one line if they remain within the line limit; do not expand short type-only imports mechanically. - Keep runtime value imports and type imports in separate declarations. @@ -598,10 +621,10 @@ and layout reuse, follow `frontend/AGENTS.md`. beginning of a line. - The TSX-specific self-review must confirm JSX closing markers and closing parentheses keep the surrounding compact style. -- The TypeScript/TSX self-review must confirm leading block indentation uses - 2 spaces per level, wrapped continuations use the repository's 4-space - continuation alignment, and complete leading runs of 8 spaces may be - compressed to tabs. +- The TypeScript/TSX self-review must confirm block indentation uses 2 spaces + per level, wrapped continuations use the repository's 4-space continuation + alignment, and every complete run of 8 spaces used for indentation or + alignment has been compressed to tabs. - Prefer `const` arrow functions for TypeScript/TSX component and helper declarations. - Put two blank lines before and after top-level `const` function declarations, unless imports, exports, or file boundaries make that awkward. @@ -614,9 +637,18 @@ and layout reuse, follow `frontend/AGENTS.md`. - Indent the block body 2 spaces deeper than the keyword and opening brace. - Put the closing `}` on its own line at the same indentation as the keyword. - Do not write `try {`, `catch {`, or `finally {`. -- In TypeScript and TSX, convert every complete leading run of 8 spaces to a - tab character. -- A leading tab is exactly equivalent to 8 leading spaces. +- In TypeScript and TSX, convert every complete run of 8 spaces used for + indentation or alignment to a tab character. +- In TypeScript and TSX, a tab is exactly equivalent to 8 columns, whether it + appears at the beginning of a line or in alignment whitespace after + non-space text. +- In TSX, JSX nesting uses 2-space block indentation and wrapped JSX + attributes use 4-space continuation indentation; after visible columns are + determined, compress every complete run of 8 spaces in the resulting + indentation or alignment to tabs. +- In TSX, do not leave JSX subtree indentation at 8, 10, 12, 14, or 16 + columns as spaces alone; convert each complete run of 8 spaces to tabs and + keep only the residual 2, 4, or 6 spaces. - In TypeScript and TSX function declarations, including `const` arrow function declarations, classify the parameter list before placing the closing `)`. @@ -1259,9 +1291,9 @@ to `.ts` and `.tsx`: 7. JSX `>` and `/>` stay with the final prop unless nearby code proves otherwise. 8. JSX closing parentheses keep the compact local style. -9. Leading block indentation uses 2 spaces per level, wrapped continuations - use the repository's 4-space continuation alignment, and complete leading - runs of 8 spaces may be compressed to tabs. +9. Block indentation uses 2 spaces per level, wrapped continuations use the + repository's 4-space continuation alignment, and every complete run of 8 + spaces used for indentation or alignment has been compressed to tabs. 10. No line has trailing whitespace. Preferred: diff --git a/backend/app/models/post.rb b/backend/app/models/post.rb index d346d41..69f7e81 100644 --- a/backend/app/models/post.rb +++ b/backend/app/models/post.rb @@ -138,7 +138,8 @@ 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) : '' }]" + "[#{ Post.ms_to_time(section.begin_ms) }-"\ + "#{ section.end_ms ? Post.ms_to_time(section.end_ms) : '' }]" end def self.ms_to_time ms @@ -323,7 +324,9 @@ class Post < ApplicationRecord end def self.external_svg_reference?(attribute_name, attribute_value) - return external_svg_url?(attribute_value) if ['href', 'xlink:href', 'src'].include?(attribute_name) + if ['href', 'xlink:href', 'src'].include?(attribute_name) + return external_svg_url?(attribute_value) + end return style_contains_disallowed_urls?(attribute_value) if attribute_name == 'style' return svg_url_function_disallowed?(attribute_value) if attribute_value.match?(/url\s*\(/i) @@ -344,7 +347,12 @@ class Post < ApplicationRecord def self.svg_url_function_disallowed?(value) value.to_s.scan(/url\s*\(([^)]*)\)/i).flatten.any? do |entry| - reference = entry.to_s.strip.delete_prefix("'").delete_prefix('"').delete_suffix("'").delete_suffix('"') + reference = + entry.to_s.strip + .delete_prefix("'") + .delete_prefix('"') + .delete_suffix("'") + .delete_suffix('"') reference.present? && !(reference.start_with?('#')) end end @@ -433,7 +441,12 @@ class Post < ApplicationRecord match = value.match( - /\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) return nil if match.nil? year = match[1].to_i diff --git a/backend/app/services/post_create_plan.rb b/backend/app/services/post_create_plan.rb index 96790fe..a6f38b7 100644 --- a/backend/app/services/post_create_plan.rb +++ b/backend/app/services/post_create_plan.rb @@ -85,7 +85,9 @@ class PostCreatePlan end_raw: match[3], tag_name: name) end - raise Tag::SectionLiteralParseError.new(raw_name, raw_name) if name.include?('[') || name.include?(']') + if name.include?('[') || name.include?(']') + raise Tag::SectionLiteralParseError.new(raw_name, raw_name) + end [resolved_tag_name(name), category&.to_sym, sections] end diff --git a/backend/spec/requests/posts_spec.rb b/backend/spec/requests/posts_spec.rb index 537a093..ef98148 100644 --- a/backend/spec/requests/posts_spec.rb +++ b/backend/spec/requests/posts_spec.rb @@ -124,7 +124,7 @@ RSpec.describe 'Posts API', type: :request do end end - it "returns posts with tag name in JSON" do + it 'returns posts with tag name in JSON' do get "/posts" expect(response).to have_http_status(:ok) @@ -161,9 +161,9 @@ RSpec.describe 'Posts API', type: :request do ]) end - context "when q is provided" do - it "filters posts by q (hit case)" do - get "/posts", params: { tags: "spec_tag" } + context 'when q is provided' do + it 'filters posts by q (hit case)' do + get '/posts', params: { tags: 'spec_tag' } expect(response).to have_http_status(:ok) posts = json.fetch('posts') @@ -181,8 +181,8 @@ RSpec.describe 'Posts API', type: :request do end end - it "filters posts by q (hit case by alias)" do - get "/posts", params: { tags: "manko" } + it 'filters posts by q (hit case by alias)' do + get '/posts', params: { tags: 'manko' } expect(response).to have_http_status(:ok) posts = json.fetch('posts') @@ -200,7 +200,7 @@ RSpec.describe 'Posts API', type: :request do end end - it "returns empty posts when nothing matches" do + it 'returns empty posts when nothing matches' do get "/posts", params: { tags: "no_such_keyword_12345" } expect(response).to have_http_status(:ok) @@ -1161,7 +1161,7 @@ RSpec.describe 'Posts API', type: :request do ) end - context "when nico tag already exists in tags" do + context 'when nico tag already exists in tags' do before do Tag.find_undiscard_or_create_by!( tag_name: TagName.find_undiscard_or_create_by!(name: 'nico:nico_tag'), @@ -1455,7 +1455,7 @@ RSpec.describe 'Posts API', type: :request do ) end - context "when nico tag already exists in tags" do + context 'when nico tag already exists in tags' do before do Tag.find_undiscard_or_create_by!( tag_name: TagName.find_undiscard_or_create_by!(name: 'nico:nico_tag'), @@ -1712,7 +1712,8 @@ 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 26b3bb3..88f507d 100644 --- a/backend/spec/tasks/nico_sync_spec.rb +++ b/backend/spec/tasks/nico_sync_spec.rb @@ -16,11 +16,11 @@ RSpec.describe "nico:sync" do NicoTagRelation.create!(nico_tag_id: nico_tag.id, tag_id: tag.id) end - it "既存 post を見つけて、nico tag と linked tag を追加し、差分が出たら bot を付ける" do + it '既存 post を見つけて、nico tag と linked tag を追加し、差分が出たら bot を付ける' do # 既存 post(正規表現で拾われるURL) post = Post.create!( - title: "old", - url: "https://www.nicovideo.jp/watch/sm9", + title: 'old', + url: 'https://www.nicovideo.jp/watch/sm9', uploaded_user: nil ) @@ -108,10 +108,10 @@ RSpec.describe "nico:sync" do expect(calls).to eq(2) end - it "既存 post にあった古い nico tag は active から外され、履歴として discard される" do + it '既存 post にあった古い nico tag は active から外され、履歴として discard される' do post = Post.create!( - title: "old", - url: "https://www.nicovideo.jp/watch/sm9", + title: 'old', + url: 'https://www.nicovideo.jp/watch/sm9', uploaded_user: nil ) diff --git a/frontend/src/components/PostEditForm.test.tsx b/frontend/src/components/PostEditForm.test.tsx index 769d04e..349c753 100644 --- a/frontend/src/components/PostEditForm.test.tsx +++ b/frontend/src/components/PostEditForm.test.tsx @@ -92,7 +92,9 @@ describe ('PostEditForm', () => { .toHaveValue ('180.5') }) - it ('shows deduplicated original-created endpoint errors on the shared datetime field', async () => { + it ( + 'shows deduplicated original-created endpoint errors on the shared datetime field', + async () => { const post = buildPost () api.isApiError.mockReturnValue (true) postsApi.updatePost.mockRejectedValueOnce ({ @@ -114,5 +116,5 @@ describe ('PostEditForm', () => { expect (await screen.findByText ('日時を確認してください.')).toBeInTheDocument () expect (screen.getByText ('終了を確認してください.')).toBeInTheDocument () expect (screen.getAllByText ('日時を確認してください.')).toHaveLength (1) - }) + }) }) diff --git a/frontend/src/components/TagLink.tsx b/frontend/src/components/TagLink.tsx index cfe088e..455add6 100644 --- a/frontend/src/components/TagLink.tsx +++ b/frontend/src/components/TagLink.tsx @@ -75,8 +75,6 @@ const TagLink: FC = ({ tag, const spanClass = 'tag-link-colour' const linkClass = 'tag-link-colour tag-link-hover-colour' const textClass = 'group min-w-0 max-w-full overflow-hidden align-bottom' - const rootClass = - 'inline-flex min-w-0 max-w-full flex-nowrap items-stretch align-baseline gap-x-1 md:items-baseline' const markerWrapClass = 'shrink-0 self-start md:self-auto' const countClass = 'shrink-0 self-end md:self-auto' const matchedAlias = isFullTag (tag) ? tag.matchedAlias : null @@ -84,7 +82,10 @@ const TagLink: FC = ({ tag, ?? (matchedAlias == null ? tag.name : `${ matchedAlias } → ${ tag.name }`) return ( - + {(linkFlg && withWiki && isFullTag (tag)) && ( {(tag.materialId != null || tag.hasWiki || tag.hasDeerjikists) diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx index b17b860..e3c154c 100644 --- a/frontend/src/components/ui/dialog.tsx +++ b/frontend/src/components/ui/dialog.tsx @@ -1,10 +1,10 @@ -"use client" +'use client' -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { X } from "lucide-react" +import * as DialogPrimitive from '@radix-ui/react-dialog' +import { X } from 'lucide-react' +import * as React from 'react' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' const Dialog = DialogPrimitive.Root @@ -15,111 +15,109 @@ const DialogPortal = DialogPrimitive.Portal const DialogClose = DialogPrimitive.Close const DialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - )) + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ({ className, ...props }, ref) => ( + )) DialogOverlay.displayName = DialogPrimitive.Overlay.displayName const DialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - - {children} + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ({ className, children, ...props }, ref) => ( + + + + {children} - - - 閉ぢる - - - )) + + + 閉ぢる + + + )) DialogContent.displayName = DialogPrimitive.Content.displayName -const DialogHeader = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
) -DialogHeader.displayName = "DialogHeader" +const DialogHeader = ( + { className, ...props }: React.HTMLAttributes, +) => ( +
) +DialogHeader.displayName = 'DialogHeader' -const DialogFooter = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
) -DialogFooter.displayName = "DialogFooter" +const DialogFooter = ( + { className, ...props }: React.HTMLAttributes, +) => ( +
) +DialogFooter.displayName = 'DialogFooter' const DialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - )) + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ({ className, ...props }, ref) => ( + )) DialogTitle.displayName = DialogPrimitive.Title.displayName const DialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - )) + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ({ className, ...props }, ref) => ( + )) DialogDescription.displayName = DialogPrimitive.Description.displayName export { - Dialog, - DialogPortal, - DialogOverlay, - DialogClose, - DialogTrigger, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, } diff --git a/frontend/src/pages/theatres/TheatreDetailPage.test.tsx b/frontend/src/pages/theatres/TheatreDetailPage.test.tsx index 1ff33a2..0a78f51 100644 --- a/frontend/src/pages/theatres/TheatreDetailPage.test.tsx +++ b/frontend/src/pages/theatres/TheatreDetailPage.test.tsx @@ -262,7 +262,9 @@ describe ('TheatreDetailPage', () => { expect (postEmbed.seek).not.toHaveBeenCalledWith (0) }) - it ('shows child tags from the post tag tree in both vertical and horizontal layouts', async () => { + it ( + 'shows child tags from the post tag tree in both vertical and horizontal layouts', + async () => { const childTag = buildTag ({ id: 12, name: '子タグ', category: 'general' }) const parentTag = buildTag ({ id: 11, @@ -307,7 +309,7 @@ describe ('TheatreDetailPage', () => { expect (within (tagSection ()).getAllByRole ('link', { name: '子タグ' })) .toHaveLength (1) }) - }) + }) it ('does not advance host post while video length is unknown', async () => { api.apiPut.mockImplementation ((path: string) => {