forks/sorcery
git clone https://git.gae.moe/forks/sorcery
7e9177b
typst_support
1/** 2* where views live. the static page is the fallback and the hash is a delta 3* on it: pieces missing from the hash (revision, path, kind) come from the 4* pathname, so `/u/r/ref/main/blob/x#history` is x's history from main's tip, 5* `/u/r/ref/main/blob/x#<oid>` is x at that commit, and `/u/r/ref/main/blob/x` 6* alone is x at the tip. 7*/ 8import type { GitRepo } from "./git/repo.ts" ; 9 10export type ViewKind = "tree" | "blob" | "history" | "commit" | "language" ; 11/** for `language`, `path` is the single language id */ 12export interface View { 13kind :ViewKind ; 14oid :string ; 15path :string []; 16} 17 18/** what the pathname says, plus the ref it stands for */ 19export interface Page { 20base :string ; 21/** the ref's root page: `/u/r/` when that is where we are, else `/u/r/ref/<ref>/` */ 22rootBase :string ; 23/** where the ref's tree and blob pages live: always `/u/r/ref/<ref>/` */ 24refBase :string ; 25tip :string | null ; 26kind :"tree" | "blob" ; 27path :string []; 28} 29 30export interface Site { 31repo :GitRepo ; 32page :Page ; 33name :string ; 34} 35 36const OID = "([0-9a-f]{40}|[0-9a-f]{64})" ; 37export const encodePath = ( path :string []) => path . map ( encodeURIComponent ). join ( "/" ); 38const decodePath = ( path :string ) => path . split ( "/" ). filter ( Boolean ). map ( decodeURIComponent ); 39 40/** `ref` and `tip` come from `<main data-ref data-tip>`; the ref's pages are 41* the only ones `tip` speaks for, see `withinRef` */ 42export function pageAt ( pathname :string , ref :string | null , tip :string | null ) :Page { 43const [ user , repo , ...rest ] = pathname . split ( "/" ). filter ( Boolean ); 44const base = `/ ${ user } / ${ repo } ` ; 45if ( rest [ 0 ] !== "ref" ) { 46const refBase = ref ?` ${ base } /ref/ ${ encodePath ( ref . split ( "/" ))} /` :` ${ base } /` ; 47// only the root page shows the ref; `/u/r/refs` and the like do not 48return { base, rootBase :` ${ base } /` , refBase, tip :rest . length === 0 ?tip :null , kind :"tree" , path :[] }; 49} 50// ref names may contain slashes, so the ref runs up to the tree/blob marker 51const marker = rest . findIndex (( seg , i ) => i > 1 && ( seg === "tree" || seg === "blob" )); 52const refBase = ` ${ base } / ${( marker < 0 ? rest : rest . slice ( 0 , marker )). join ( "/" )} /` ; 53const page = { base, rootBase :refBase , refBase, tip}; 54if ( marker < 0 ) return { ...page , kind :"tree" , path :[] }; 55return { ...page , kind :rest [ marker ] as "tree" | "blob" , path :decodePath ( rest . slice ( marker + 1 ). join ( "/" )) }; 56} 57 58/** whether `pathname` is a page of the same ref as `page`, so that `page.tip` 59* stands for it too */ 60export const withinRef = ( page :Page , pathname :string ) => 61page . tip !== null && ( pathname === page . rootBase || pathname . startsWith ( page . refBase )); 62 63/** what the pathname alone shows: the page's own path at the tip */ 64export const pageView = ( page :Page ) :View | null => 65page . tip ?{ kind :page . kind , oid :page . tip , path :page . path } :null ; 66 67export const viewKey = ( view :View | null ) => view ?[ view . kind , view . oid , ...view . path ]. join ( "/" ) :"" ; 68 69/** whether `view` is what the page's pathname alone shows, i.e. its url has no hash */ 70export const isPageView = ( page :Page , view :View ) => viewKey ( view ) === viewKey ( pageView ( page )); 71 72/** the hash as a delta on the page; a hash that is no route (`#L12`) leaves 73* the page's own view */ 74export function viewAt ( page :Page , hash :string ) :View | null { 75const match = ( pattern :string ) => hash . match ( new RegExp ( `^# ${ pattern } $` )); 76let m :RegExpMatchArray | null ; 77if (( m = match ( OID ))) return { kind :page . kind , oid :m [ 1 ], path :page . path }; 78if (( m = match ( `commit/ ${ OID } ` ))) return { kind :"commit" , oid :m [ 1 ], path :[] }; 79if (( m = match ( `history(?:/ ${ OID } )?` ))) { 80const oid = m [ 1 ] ?? page . tip ; 81return oid ?{ kind :"history" , oid, path :page . path } :null ; 82} 83if (( m = match ( `language/([a-z0-9-]+)(?:/ ${ OID } )?` ))) { 84const oid = m [ 2 ] ?? page . tip ; 85return oid ?{ kind :"language" , oid, path :[ m [ 1 ]] } :null ; 86} 87if (( m = match ( `(tree|blob|history)/ ${ OID } /(.*)` ))) { 88return { kind :m [ 1 ] as ViewKind , oid :m [ 2 ], path :decodePath ( m [ 3 ]) }; 89} 90if (( m = match ( `tree/ ${ OID } ` ))) return { kind :"tree" , oid :m [ 1 ], path :[] }; 91return pageView ( page ); 92} 93 94/** 95* the shortest url for `view` that is still a real page without JS: the 96* static page itself when it shows the same thing, else the closest static 97* page with the difference in the hash. `atTip` is what `view.path` is at 98* the page's tip, if anything; hash-only results keep the current pathname. 99*/ 100export function canonical ( page :Page , view :View , atTip :"tree" | "blob" | null ) :string { 101const { kind, oid, path} = view ; 102const rev = oid === page . tip ?"" :`/ ${ oid } ` ; 103const pageFor = ( kind :"tree" | "blob" ) => 104kind === "blob" 105 ?` ${ page . refBase } blob/ ${ encodePath ( path )} ` 106 :path . length > 0 107 ?` ${ page . refBase } tree/ ${ encodePath ( path )} /` 108 :page . rootBase ; 109switch ( kind ) { 110case "commit" : 111return `#commit/ ${ oid } ` ; 112case "language" : 113return ` ${ page . rootBase } #language/ ${ path [ 0 ]} ${ rev } ` ; 114case "history" : 115return atTip ?` ${ pageFor ( atTip )} #history ${ rev } ` :`#history/ ${ oid } / ${ encodePath ( path )} ` ; 116default : 117return atTip === kind 118 ?` ${ pageFor ( kind )} ${ oid === page . tip ? "" : `# ${ oid } ` } ` 119 :`# ${ kind } / ${ oid } / ${ encodePath ( path )} ` ; 120} 121}