| import * as Y from "yjs"; |
|
|
| export interface Author { |
| name: string; |
| url?: string; |
| affiliations: number[]; |
| } |
|
|
| export interface Affiliation { |
| name: string; |
| url?: string; |
| } |
|
|
| export interface LinkItem { |
| label: string; |
| url: string; |
| } |
|
|
| export interface FrontmatterData { |
| title: string; |
| subtitle: string; |
| description: string; |
| authors: Author[]; |
| affiliations: Affiliation[]; |
| published: string; |
| template: "article" | "paper"; |
| banner: string; |
| doi: string; |
| showPdf: boolean; |
| tableOfContentsAutoCollapse: boolean; |
| licence: string; |
| pdfProOnly: boolean; |
| seoThumbImage: string; |
| links: LinkItem[]; |
| } |
|
|
| const SCALAR_DEFAULTS: Omit<FrontmatterData, "authors" | "affiliations" | "links"> = { |
| title: "", |
| subtitle: "", |
| description: "", |
| published: "", |
| template: "article", |
| banner: "banner.html", |
| doi: "", |
| showPdf: true, |
| tableOfContentsAutoCollapse: false, |
| licence: "", |
| pdfProOnly: false, |
| seoThumbImage: "", |
| }; |
|
|
| type ScalarKey = keyof typeof SCALAR_DEFAULTS; |
| type ArrayField = "authors" | "affiliations" | "links"; |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function createFrontmatterStore(ydoc: Y.Doc) { |
| const ymap = ydoc.getMap("frontmatter"); |
| const yAuthors = ydoc.getArray<Author>("frontmatter.authors"); |
| const yAffiliations = ydoc.getArray<Affiliation>("frontmatter.affiliations"); |
| const yLinks = ydoc.getArray<LinkItem>("frontmatter.links"); |
|
|
| |
|
|
| function getScalar<K extends ScalarKey>(key: K): FrontmatterData[K] { |
| const val = ymap.get(key); |
| return val !== undefined ? (val as FrontmatterData[K]) : (SCALAR_DEFAULTS[key] as FrontmatterData[K]); |
| } |
|
|
| function setScalar<K extends ScalarKey>(key: K, value: FrontmatterData[K]) { |
| ymap.set(key, value); |
| } |
|
|
| |
|
|
| function getAll(): FrontmatterData { |
| const scalars: Record<string, unknown> = {}; |
| for (const key of Object.keys(SCALAR_DEFAULTS) as ScalarKey[]) { |
| const val = ymap.get(key); |
| scalars[key] = val !== undefined ? val : SCALAR_DEFAULTS[key]; |
| } |
|
|
| return { |
| ...(scalars as Omit<FrontmatterData, "authors" | "affiliations" | "links">), |
| authors: yAuthors.toArray(), |
| affiliations: yAffiliations.toArray(), |
| links: yLinks.toArray(), |
| }; |
| } |
|
|
| function setAll(data: Partial<FrontmatterData>) { |
| ydoc.transact(() => { |
| |
| |
| if (data.authors !== undefined) { |
| yAuthors.delete(0, yAuthors.length); |
| yAuthors.push(data.authors); |
| } |
| if (data.affiliations !== undefined) { |
| yAffiliations.delete(0, yAffiliations.length); |
| yAffiliations.push(data.affiliations); |
| } |
| if (data.links !== undefined) { |
| yLinks.delete(0, yLinks.length); |
| yLinks.push(data.links); |
| } |
|
|
| for (const [key, value] of Object.entries(data)) { |
| if (value === undefined) continue; |
| if (key === "authors" || key === "affiliations" || key === "links") continue; |
| ymap.set(key, value); |
| } |
| }); |
| } |
|
|
| |
|
|
| function get<K extends keyof FrontmatterData>(key: K): FrontmatterData[K] { |
| if (key === "authors") return yAuthors.toArray() as FrontmatterData[K]; |
| if (key === "affiliations") return yAffiliations.toArray() as FrontmatterData[K]; |
| if (key === "links") return yLinks.toArray() as FrontmatterData[K]; |
| return getScalar(key as ScalarKey) as FrontmatterData[K]; |
| } |
|
|
| function set<K extends keyof FrontmatterData>(key: K, value: FrontmatterData[K]) { |
| if (key === "authors") { |
| ydoc.transact(() => { |
| yAuthors.delete(0, yAuthors.length); |
| yAuthors.push(value as Author[]); |
| }); |
| } else if (key === "affiliations") { |
| ydoc.transact(() => { |
| yAffiliations.delete(0, yAffiliations.length); |
| yAffiliations.push(value as Affiliation[]); |
| }); |
| } else if (key === "links") { |
| ydoc.transact(() => { |
| yLinks.delete(0, yLinks.length); |
| yLinks.push(value as LinkItem[]); |
| }); |
| } else { |
| setScalar(key as ScalarKey, value as FrontmatterData[ScalarKey]); |
| } |
| } |
|
|
| |
|
|
| function addAuthor(author: Author) { |
| yAuthors.push([author]); |
| } |
|
|
| function updateAuthor(index: number, author: Author) { |
| if (index >= 0 && index < yAuthors.length) { |
| ydoc.transact(() => { |
| yAuthors.delete(index, 1); |
| yAuthors.insert(index, [author]); |
| }); |
| } |
| } |
|
|
| function removeAuthor(index: number) { |
| if (index >= 0 && index < yAuthors.length) { |
| yAuthors.delete(index, 1); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| function moveAuthor(from: number, to: number) { |
| const len = yAuthors.length; |
| if (from === to || from < 0 || to < 0 || from >= len || to >= len) return; |
| ydoc.transact(() => { |
| const item = yAuthors.get(from); |
| yAuthors.delete(from, 1); |
| yAuthors.insert(to, [item]); |
| }); |
| } |
|
|
| |
|
|
| function addAffiliation(affiliation: Affiliation): number { |
| yAffiliations.push([affiliation]); |
| return yAffiliations.length; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function moveAffiliation(from: number, to: number) { |
| const len = yAffiliations.length; |
| if (from === to || from < 0 || to < 0 || from >= len || to >= len) return; |
| ydoc.transact(() => { |
| const aff = yAffiliations.get(from); |
| yAffiliations.delete(from, 1); |
| yAffiliations.insert(to, [aff]); |
|
|
| |
| const order = Array.from({ length: len }, (_, i) => i); |
| const [moved] = order.splice(from, 1); |
| order.splice(to, 0, moved); |
| const oldToNew = new Map<number, number>(); |
| order.forEach((oldPos, newPos) => oldToNew.set(oldPos + 1, newPos + 1)); |
|
|
| const authors = yAuthors.toArray(); |
| yAuthors.delete(0, yAuthors.length); |
| yAuthors.push( |
| authors.map((a) => ({ |
| ...a, |
| affiliations: (a.affiliations || []).map((idx) => oldToNew.get(idx) ?? idx), |
| })), |
| ); |
| }); |
| } |
|
|
| function updateAffiliation(index: number, affiliation: Affiliation) { |
| if (index >= 0 && index < yAffiliations.length) { |
| ydoc.transact(() => { |
| yAffiliations.delete(index, 1); |
| yAffiliations.insert(index, [affiliation]); |
| }); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| function removeAffiliation(index: number) { |
| const len = yAffiliations.length; |
| if (index < 0 || index >= len) return; |
| const removed = index + 1; |
| ydoc.transact(() => { |
| yAffiliations.delete(index, 1); |
| const authors = yAuthors.toArray(); |
| yAuthors.delete(0, yAuthors.length); |
| yAuthors.push( |
| authors.map((a) => ({ |
| ...a, |
| affiliations: (a.affiliations || []) |
| .filter((idx) => idx !== removed) |
| .map((idx) => (idx > removed ? idx - 1 : idx)), |
| })), |
| ); |
| }); |
| } |
|
|
| |
|
|
| function observe(callback: () => void) { |
| ymap.observe(callback); |
| yAuthors.observe(callback); |
| yAffiliations.observe(callback); |
| yLinks.observe(callback); |
|
|
| return () => { |
| ymap.unobserve(callback); |
| yAuthors.unobserve(callback); |
| yAffiliations.unobserve(callback); |
| yLinks.unobserve(callback); |
| }; |
| } |
|
|
| return { |
| get, |
| set, |
| getAll, |
| setAll, |
| addAuthor, |
| updateAuthor, |
| removeAuthor, |
| moveAuthor, |
| addAffiliation, |
| moveAffiliation, |
| updateAffiliation, |
| removeAffiliation, |
| observe, |
| }; |
| } |
|
|
| export type FrontmatterStore = ReturnType<typeof createFrontmatterStore>; |
|
|