@@ -26,31 +26,38 @@ jobs:
2626 run : |
2727 python <<'PY'
2828 from pathlib import Path
29+ from textwrap import dedent
30+
31+ def replace_once(source: str, old: str, new: str, label: str) -> str:
32+ count = source.count(old)
33+ if count != 1:
34+ raise SystemExit(f'{label}: expected 1 match, found {count}')
35+ return source.replace(old, new, 1)
2936
3037 page = Path('src/pages/BookmarkLibrary.tsx')
3138 source = page.read_text(encoding='utf-8')
3239
33- replacements = [
34- (
35- " useDeferredValue,\n useMemo,\n useState,\n",
36- " useDeferredValue,\n useEffect,\n useMemo,\n useRef,\n useState,\n",
37- ) ,
38- (
39- " ArrowUpDown,\n Clock3,\n",
40- " ArrowUpDown,\n ChevronLeft,\n ChevronRight,\n Clock3,\n" ,
41- ) ,
42- (
43- " List,\n Lock,\n" ,
44- " List,\n Lock,\n MoreHorizontal,\n",
45- ),
46- ]
47- for old, new in replacements:
48- if source.count(old) != 1:
49- raise SystemExit(f'BookmarkLibrary import anchor mismatch: {old!r}')
50- source = source.replace(old, new, 1 )
51-
52- constants_anchor = "const LIBRARY_VIEW_KEY = 'nowen-library-view-v1'\nconst LIBRARY_SORT_KEY = 'nowen-library-sort-v1'\n"
53- constants_replacement = """ const LIBRARY_VIEW_KEY = 'nowen-library-view-v1'
40+ source = replace_once(
41+ source,
42+ " useDeferredValue,\n useMemo,\n useState,\n",
43+ " useDeferredValue,\n useEffect,\n useMemo,\n useRef,\n useState,\n",
44+ 'react imports' ,
45+ )
46+ source = replace_once(
47+ source ,
48+ " ArrowUpDown,\n Clock3,\n" ,
49+ " ArrowUpDown,\n ChevronLeft,\n ChevronRight,\n Clock3,\n",
50+ 'chevron imports' ,
51+ )
52+ source = replace_once(
53+ source,
54+ " List,\n Lock,\n",
55+ " List,\n Lock,\n MoreHorizontal,\n",
56+ 'ellipsis import',
57+ )
58+
59+ constants = dedent('''
60+ const LIBRARY_VIEW_KEY = 'nowen-library-view-v1'
5461 const LIBRARY_SORT_KEY = 'nowen-library-sort-v1'
5562 const LIBRARY_PAGE_SIZE_KEY = 'nowen-library-page-size-v1'
5663 const LIBRARY_PAGE_SIZE_OPTIONS = [20, 40, 80] as const
@@ -94,26 +101,28 @@ jobs:
94101 items.push(totalPages)
95102 return items
96103 }
97- """
98- if source.count(constants_anchor) != 1:
99- raise SystemExit('BookmarkLibrary constants anchor mismatch')
100- source = source.replace(constants_anchor, constants_replacement, 1)
104+ ''').lstrip()
105+ source = replace_once(
106+ source,
107+ "const LIBRARY_VIEW_KEY = 'nowen-library-view-v1'\nconst LIBRARY_SORT_KEY = 'nowen-library-sort-v1'\n",
108+ constants,
109+ 'library constants',
110+ )
101111
102- state_anchor = """ const [sortMode, setSortMode] = useState<SortMode>(() =>
103- readStoredValue(LIBRARY_SORT_KEY, ['custom', 'title', 'updated'] as const, 'custom'),
112+ state_anchor = (
113+ " const [sortMode, setSortMode] = useState<SortMode>(() =>\n"
114+ " readStoredValue(LIBRARY_SORT_KEY, ['custom', 'title', 'updated'] as const, 'custom'),\n"
115+ " )\n"
104116 )
105- """
106- state_replacement = state_anchor + """ const [pageSize, setPageSize] = useState(readStoredPageSize)
107- const [currentPage, setCurrentPage] = useState(1)
108- const contentRef = useRef<HTMLElement>(null)
109- """
110- if source.count(state_anchor) != 1:
111- raise SystemExit('BookmarkLibrary state anchor mismatch')
112- source = source.replace(state_anchor, state_replacement, 1)
113-
114- logic_anchor = """ const activeCollectionLabel = useMemo(() => {
115- """
116- logic_block = """ const totalPages = Math.max(1, Math.ceil(filteredBookmarks.length / pageSize))
117+ state_insert = state_anchor + (
118+ " const [pageSize, setPageSize] = useState(readStoredPageSize)\n"
119+ " const [currentPage, setCurrentPage] = useState(1)\n"
120+ " const contentRef = useRef<HTMLElement>(null)\n"
121+ )
122+ source = replace_once(source, state_anchor, state_insert, 'pagination state')
123+
124+ pagination_logic = dedent('''
125+ const totalPages = Math.max(1, Math.ceil(filteredBookmarks.length / pageSize))
117126 const safeCurrentPage = Math.min(currentPage, totalPages)
118127 const pageStart = (safeCurrentPage - 1) * pageSize
119128 const paginatedBookmarks = useMemo(
@@ -136,29 +145,33 @@ jobs:
136145 setCurrentPage((previousPage) => Math.min(previousPage, totalPages))
137146 }, [totalPages])
138147
139- """ + logic_anchor
140- if source.count(logic_anchor) != 1:
141- raise SystemExit('BookmarkLibrary pagination logic anchor mismatch')
142- source = source.replace(logic_anchor, logic_block, 1)
143-
144- handlers_anchor = """ const clearFilters = useCallback(() => {
145- setQuery('')
146- if (activeTag) {
147- onSelectTag(null)
148- } else if (activeCollection !== 'all') {
149- onSelectCollection('all')
150- }
151- }, [activeCollection, activeTag, onSelectCollection, onSelectTag])
152-
153- """
154- handlers_replacement = handlers_anchor + """ const scrollToResults = useCallback(() => {
155- const scroll = () => contentRef.current?.scrollIntoView?.({ behavior: 'smooth', block: 'start' })
156- if (typeof window.requestAnimationFrame === 'function') {
157- window.requestAnimationFrame(scroll)
158- } else {
159- scroll()
160- }
161- }, [])
148+ ''')
149+ source = replace_once(
150+ source,
151+ " const activeCollectionLabel = useMemo(() => {\n",
152+ pagination_logic + " const activeCollectionLabel = useMemo(() => {\n",
153+ 'pagination calculations',
154+ )
155+
156+ clear_filters = (
157+ " const clearFilters = useCallback(() => {\n"
158+ " setQuery('')\n"
159+ " if (activeTag) {\n"
160+ " onSelectTag(null)\n"
161+ " } else if (activeCollection !== 'all') {\n"
162+ " onSelectCollection('all')\n"
163+ " }\n"
164+ " }, [activeCollection, activeTag, onSelectCollection, onSelectTag])\n\n"
165+ )
166+ pagination_handlers = dedent('''
167+ const scrollToResults = useCallback(() => {
168+ const scroll = () => contentRef.current?.scrollIntoView?.({ behavior: 'smooth', block: 'start' })
169+ if (typeof window.requestAnimationFrame === 'function') {
170+ window.requestAnimationFrame(scroll)
171+ } else {
172+ scroll()
173+ }
174+ }, [])
162175
163176 const changePage = useCallback((nextPage: number) => {
164177 const clampedPage = Math.min(Math.max(nextPage, 1), totalPages)
@@ -174,39 +187,22 @@ jobs:
174187 scrollToResults()
175188 }, [scrollToResults])
176189
177- """
178- if source.count(handlers_anchor) != 1:
179- raise SystemExit('BookmarkLibrary handlers anchor mismatch')
180- source = source.replace(handlers_anchor, handlers_replacement, 1)
181-
182- main_anchor = '<main className="bookmark-library__content">'
183- if source.count(main_anchor) != 1:
184- raise SystemExit('BookmarkLibrary main anchor mismatch')
185- source = source.replace(main_anchor, '<main ref={contentRef} className="bookmark-library__content">', 1)
186-
187- results_anchor = """ {filteredBookmarks.map((bookmark) => (
188- """
189- if source.count(results_anchor) != 1:
190- raise SystemExit('BookmarkLibrary result map anchor mismatch')
191- source = source.replace(results_anchor, """ {paginatedBookmarks.map((bookmark) => (
192- """, 1)
193-
194- footer_anchor = """ ))}
195- </div>
196- ) : (
197- """
198- footer_replacement = """ ))}
199- </div>
200- ) : (
201- """
202- if source.count(footer_anchor) != 1:
203- raise SystemExit('BookmarkLibrary result footer anchor mismatch')
204-
205- # Insert the pagination after the result/empty conditional so the footer stays stable.
206- conditional_end_anchor = """ )}
207- </main>
208- " " "
209- pagination_markup = " " " )}
190+ ''')
191+ source = replace_once(source, clear_filters, clear_filters + pagination_handlers, 'pagination handlers')
192+ source = replace_once(
193+ source,
194+ '<main className="bookmark-library__content">',
195+ '<main ref={contentRef} className="bookmark-library__content">',
196+ 'content ref',
197+ )
198+ source = replace_once(
199+ source,
200+ ' {filteredBookmarks.map((bookmark) => (\n',
201+ ' {paginatedBookmarks.map((bookmark) => (\n',
202+ 'paginated result map',
203+ )
204+
205+ pagination_markup = dedent('''
210206
211207 {showPagination && (
212208 <footer className="bookmark-library__pagination">
@@ -274,18 +270,19 @@ jobs:
274270 </nav>
275271 </footer>
276272 )}
277- </main>
278- " " "
279- if source.count(conditional_end_anchor) != 1:
280- raise SystemExit('BookmarkLibrary conditional end anchor mismatch')
281- source = source.replace(conditional_end_anchor, pagination_markup, 1)
282-
273+ ''')
274+ source = replace_once(
275+ source,
276+ " )}\n </main>\n",
277+ " )}" + pagination_markup + " </main>\n",
278+ 'pagination footer',
279+ )
283280 page.write_text(source, encoding='utf-8')
284281
285282 css = Path('src/styles/bookmark-library.css')
286283 css_source = css.read_text(encoding='utf-8')
287- css_anchor = '.bookmark-library__empty { \n '
288- css_block = r''' .bookmark-library__pagination {
284+ css_block = dedent('' '
285+ .bookmark-library__pagination {
289286 display: flex;
290287 align-items: center;
291288 justify-content: space-between;
@@ -420,24 +417,27 @@ jobs:
420417 }
421418 }
422419
423- '''
424- if css_source.count(css_anchor) != 1:
425- raise SystemExit('BookmarkLibrary CSS anchor mismatch')
426- css_source = css_source.replace(css_anchor, css_block + css_anchor, 1)
420+ ''')
421+ css_source = replace_once(
422+ css_source,
423+ '.bookmark-library__empty {\n',
424+ css_block + '.bookmark-library__empty {\n',
425+ 'pagination styles',
426+ )
427427 css.write_text(css_source, encoding='utf-8')
428428
429429 test = Path('src/pages/__tests__/BookmarkLibrary.test.tsx')
430430 test_source = test.read_text(encoding='utf-8')
431- test_anchor = " " " it('persists the preferred list view', async () => {
432- const { getByTitle } = render(<BookmarkLibrary {...baseProps} />)
433- fireEvent.click( getByTitle('列表视图'))
434-
435- await waitFor(() => {
436- expect(window.localStorage.getItem('nowen-library-view-v1')).toBe('list')
437- })
438- })
439- " " "
440- test_block = test_anchor + " " "
431+ test_anchor = (
432+ " it('persists the preferred list view', async () => {\n"
433+ " const { getByTitle } = render(<BookmarkLibrary {...baseProps} />)\n"
434+ " fireEvent.click(getByTitle('列表视图'))\n\n"
435+ " await waitFor(() => {\n"
436+ " expect(window.localStorage.getItem('nowen-library-view-v1')).toBe('list')\n"
437+ " })\n"
438+ " })\n"
439+ )
440+ test_case = dedent('''
441441
442442 it('paginates large result sets and keeps page-size controls available', async () => {
443443 const manyBookmarks: Bookmark[] = Array.from({ length: 25 }, (_, index) => ({
@@ -474,10 +474,8 @@ jobs:
474474 expect(window.localStorage.getItem('nowen-library-page-size-v1')).toBe('40')
475475 })
476476 })
477- " " "
478- if test_source.count(test_anchor) != 1:
479- raise SystemExit('BookmarkLibrary test anchor mismatch')
480- test_source = test_source.replace(test_anchor, test_block, 1)
477+ ''')
478+ test_source = replace_once(test_source, test_anchor, test_anchor + test_case, 'pagination test')
481479 test.write_text(test_source, encoding='utf-8')
482480 PY
483481
0 commit comments