@@ -4,12 +4,15 @@ import { join } from "node:path";
44import yaml from "js-yaml" ;
55import { isCloudflare } from "./detectCloudflare" ;
66import { notFound } from "next/navigation" ;
7+ import crypto from "node:crypto" ;
78
89export interface MarkdownSection {
10+ file : string ; // ファイル名
911 id : string ;
1012 level : number ;
1113 title : string ;
1214 rawContent : string ; // 見出しも含めたもとのmarkdownの内容
15+ md5 : string ; // mdファイル全体のmd5
1316}
1417
1518export interface PageEntry {
@@ -36,6 +39,16 @@ interface IndexYml {
3639 } [ ] ;
3740}
3841
42+ export interface RevisionYmlEntry {
43+ page : string ;
44+ rev : SectionRevision [ ] ;
45+ }
46+ export interface SectionRevision {
47+ md5 : string ; // mdファイル全体のmd5
48+ git : string ; // git上のコミットハッシュ
49+ path : string ;
50+ }
51+
3952async function readPublicFile ( path : string ) : Promise < string > {
4053 try {
4154 if ( isCloudflare ( ) ) {
@@ -59,8 +72,8 @@ async function readPublicFile(path: string): Promise<string> {
5972
6073async function getLanguageIds ( ) : Promise < string [ ] > {
6174 if ( isCloudflare ( ) ) {
62- const raw = await readPublicFile ( "docs/languages.yml " ) ;
63- return yaml . load ( raw ) as string [ ] ;
75+ const raw = await readPublicFile ( "docs/languages.json " ) ;
76+ return JSON . parse ( raw ) as string [ ] ;
6477 } else {
6578 const docsDir = join ( process . cwd ( ) , "public" , "docs" ) ;
6679 const entries = await readdir ( docsDir , { withFileTypes : true } ) ;
@@ -90,28 +103,66 @@ export async function getPagesList(): Promise<LanguageEntry[]> {
90103 ) ;
91104}
92105
106+ export async function getSectionsList (
107+ lang : string ,
108+ pageId : string
109+ ) : Promise < string [ ] > {
110+ if ( isCloudflare ( ) ) {
111+ const sectionsJson = await readPublicFile (
112+ `docs/${ lang } /${ pageId } /sections.json`
113+ ) ;
114+ return JSON . parse ( sectionsJson ) as string [ ] ;
115+ } else {
116+ function naturalSortMdFiles ( a : string , b : string ) : number {
117+ // -intro.md always comes first
118+ if ( a === "-intro.md" ) return - 1 ;
119+ if ( b === "-intro.md" ) return 1 ;
120+ // Sort numerically by leading N1-N2 prefix
121+ const aMatch = a . match ( / ^ ( \d + ) - ( \d + ) / ) ;
122+ const bMatch = b . match ( / ^ ( \d + ) - ( \d + ) / ) ;
123+ if ( aMatch && bMatch ) {
124+ const n1Diff = parseInt ( aMatch [ 1 ] ) - parseInt ( bMatch [ 1 ] ) ;
125+ if ( n1Diff !== 0 ) return n1Diff ;
126+ return parseInt ( aMatch [ 2 ] ) - parseInt ( bMatch [ 2 ] ) ;
127+ }
128+ return a . localeCompare ( b ) ;
129+ }
130+ return ( await readdir ( join ( process . cwd ( ) , "public" , "docs" , lang , pageId ) ) )
131+ . filter ( ( f ) => f . endsWith ( ".md" ) )
132+ . sort ( naturalSortMdFiles ) ;
133+ }
134+ }
135+
136+ export async function getRevisions (
137+ sectionId : string
138+ ) : Promise < RevisionYmlEntry | undefined > {
139+ const revisionsYml = await readPublicFile ( `docs/revisions.yml` ) ;
140+ return ( yaml . load ( revisionsYml ) as Record < string , RevisionYmlEntry > ) [
141+ sectionId
142+ ] ;
143+ }
144+
93145/**
94146 * public/docs/{lang}/{pageId}/ 以下のmdファイルを結合して MarkdownSection[] を返す。
95147 */
96148export async function getMarkdownSections (
97149 lang : string ,
98150 pageId : string
99151) : Promise < MarkdownSection [ ] > {
100- const sectionsYml = await readPublicFile (
101- `docs/${ lang } /${ pageId } /sections.yml`
102- ) ;
103- const files = yaml . load ( sectionsYml ) as string [ ] ;
152+ const files = await getSectionsList ( lang , pageId ) ;
104153
105154 const sections : MarkdownSection [ ] = [ ] ;
106155 for ( const file of files ) {
107156 const raw = await readPublicFile ( `docs/${ lang } /${ pageId } /${ file } ` ) ;
108157 if ( file === "-intro.md" ) {
109158 // イントロセクションはフロントマターなし・見出しなし
110159 sections . push ( {
160+ file,
111161 id : `${ lang } -${ pageId } -intro` ,
112162 level : 1 ,
113163 title : "" ,
114164 rawContent : raw ,
165+ md5 : crypto . createHash ( "md5" ) . update ( raw ) . digest ( "base64" ) ,
115166 } ) ;
116167 } else {
117168 sections . push ( parseFrontmatter ( raw , file ) ) ;
@@ -143,9 +194,39 @@ function parseFrontmatter(content: string, file: string): MarkdownSection {
143194 . slice ( endIdx + 5 )
144195 . replace ( / - r e p l \s * \n / , `-repl:${ fm ?. id ?? "" } \n` ) ;
145196 return {
197+ file,
146198 id : fm ?. id ?? "" ,
147199 title : fm ?. title ?? "" ,
148200 level : fm ?. level ?? 2 ,
149201 rawContent,
202+ md5 : crypto . createHash ( "md5" ) . update ( rawContent ) . digest ( "base64" ) ,
150203 } ;
151204}
205+
206+ export async function getRevisionOfMarkdownSection (
207+ sectionId : string ,
208+ md5 : string
209+ ) : Promise < MarkdownSection > {
210+ const revisions = await getRevisions ( sectionId ) ;
211+ const targetRevision = revisions ?. rev . find ( ( r ) => r . md5 === md5 ) ;
212+ if ( targetRevision ) {
213+ const rawRes = await fetch (
214+ `https://raw.githubusercontent.com/ut-code/my-code/${ targetRevision . git } /${ targetRevision . path } `
215+ ) ;
216+ if ( rawRes . ok ) {
217+ const raw = await rawRes . text ( ) ;
218+ return parseFrontmatter (
219+ raw ,
220+ `${ targetRevision . git } /${ targetRevision . path } `
221+ ) ;
222+ } else {
223+ throw new Error (
224+ `Failed to fetch ${ targetRevision . git } /${ targetRevision . path } . ${ rawRes . status } : ${ await rawRes . text ( ) } `
225+ ) ;
226+ }
227+ } else {
228+ throw new Error (
229+ `Revision for sectionId=${ sectionId } , md5=${ md5 } not found`
230+ ) ;
231+ }
232+ }
0 commit comments