UNPKG

@bookbox/core

Version:

Bookbox — e-book format

886 lines (885 loc) 23.8 kB
const rootBookHeader = { value: [], text: "", key: "root", level: 0 }; function getGroup({ condition, map }) { return (schema) => { const result = []; const addContents = (schema2) => { for (const item of schema2) { if (typeof item === "string") { continue; } if (condition(item)) { result.push(map(item)); continue; } addContents(item.children); } }; addContents(schema); return result; }; } function getObjectsByHeader({ condition }) { return (schema) => { const result = {}; let currentHeader = "root"; const addContents = (schema2) => { for (const item of schema2) { if (typeof item === "string") { continue; } if (item.name === "header") { currentHeader = item.props.key; } if (condition(item)) { if (!result[currentHeader]) { result[currentHeader] = []; } result[currentHeader].push(item.props.key); continue; } addContents(item.children); } }; addContents(schema); return result; }; } const getContents = (builder, store, build) => { return getGroup({ condition: (item) => item.name === "header" || item.name === "title", map: (item) => { const headerProps = item.props; const header = { value: builder({ schema: [item], store, build }), text: item.children.map((value) => typeof value === "string" ? value : JSON.stringify(value)).join(""), key: headerProps.key, level: headerProps.level ?? 1 }; return header; } }); }; const getItemList = (name) => getGroup({ condition: (e) => e.name === name, map: (e) => e }); const getItemMeta = (schema, name) => { const imageList = getItemList(name)(schema); return { keysList: imageList.map((e) => e.props.key ?? ""), keysByHeader: getObjectsByHeader({ condition: (e) => e.name === name })(schema) }; }; function getBookMeta({ schema, store, builder, build }) { return { contents: getContents(builder, store, build)(schema), media: { image: getItemMeta(schema, "image"), video: getItemMeta(schema, "video"), audio: getItemMeta(schema, "audio") }, pages: getItemMeta(schema, "page") }; } function iterableTreeGetter(options) { const { getChildren, isLeaf } = options; return function* iterableTree(schema) { for (const item of schema) { yield item; if (isLeaf(item)) continue; yield* iterableTree(getChildren(item)); } }; } function parseNewLines(newLine) { return (text) => { const result = []; const brs = text.match(/\n\n+/g); if (!brs) { return [text]; } let tail = text; let i; for (const br of brs) { i = tail.indexOf(br); const subbrs = []; if (br.length === 2) { subbrs.push(newLine); } else { for (let j = 0; j < 2 * br.length - 4; j++) { subbrs.push(newLine); } } result.push(tail.slice(0, i), ...subbrs); tail = tail.slice(i + br.length); } if (tail) { result.push(tail); } return result; }; } function flatList(elems) { const stack = [elems]; const result = []; while (stack.length) { const current = stack.pop(); if (Array.isArray(current)) { for (let i = current.length - 1; i >= 0; i--) { stack.push(current[i]); } } else { result.push(current); } } return result; } function getPartialApply(f, args) { return (...p) => f({ ...p[0] ?? {}, ...args }); } function getByPath(path, obj) { return path.reduce((elems, name) => elems[name] ?? {}, obj); } function print(schema) { return schema.map((x) => typeof x === "string" ? x : print(x.children)).join(""); } function templateToList(text, ...elements) { const result = [text[0]]; for (let i = 1; i < text.length; i++) { result.push(elements[i - 1], text[i]); } return result; } function isTemplateParams(args) { return args[0] && Array.isArray(args[0]) && "raw" in args[0] && Array.isArray(args[0]["raw"]); } function getTextTokens(text) { const notWords = text.match(/\s+/g); if (!notWords) { return [text]; } const tokens = []; let tail = text; for (const notWord of notWords) { const i = tail.indexOf(notWord); tokens.push(tail.slice(0, i), notWord); tail = tail.slice(i + notWord.length); } if (tail) { tokens.push(tail); } return tokens; } const defaultBlockNameList = [ "strong", "em", "header", "code", "label", "link", "tooltip", "math", "external" ]; const defaultBlockNames = new Set(defaultBlockNameList); function getElementView(item) { if (typeof item === "string") { return "inline"; } const { props: userProps, name } = item; const props = { block: defaultBlockNames.has(name), ...userProps }; if (props.inline) { return "inline"; } if (props.block) { return "block"; } return props.position === "inline" ? "inline" : "block"; } const getTextLinkedItem = (text) => ({ bookElementSchema: { name: "text", props: {}, children: [] }, raw: text, previous: null, next: null, previousLeaf: null, nextLeaf: null, parent: null, firstChild: null, lastChild: null, view: "inline" }); const getPageLinkedItem = (count) => { const text = `${count}`; const textItem = getTextLinkedItem(text); return { bookElementSchema: { name: "page", props: { count, key: `page-${count}` }, children: [] }, raw: text, previous: null, next: null, previousLeaf: null, nextLeaf: null, parent: null, firstChild: textItem, lastChild: textItem, view: "inline" }; }; function bindList(items) { for (let i = 1; i < items.length; i++) { const prev = items[i - 1]; const current = items[i]; current.previous = prev; prev.next = current; } } function bindLeafList(items) { for (let i = 1; i < items.length; i++) { const prev = items[i - 1]; const current = items[i]; current.previousLeaf = prev; prev.nextLeaf = current; } } function bindParent(items, parent) { for (const childItem of items) { childItem.parent = parent; } parent.firstChild = items[0] ?? null; parent.lastChild = items[items.length - 1] ?? null; } const isNotEmpty = (text) => text !== ""; function getBookLinkedItem(item) { if (typeof item === "string") { const textItem = getTextLinkedItem(item); const children2 = getTextTokens(item).filter(isNotEmpty).map(getTextLinkedItem); bindList(children2); bindParent(children2, textItem); return textItem; } const linkedItem = { bookElementSchema: item, firstChild: null, lastChild: null, raw: null, previous: null, next: null, previousLeaf: null, nextLeaf: null, parent: null, view: getElementView(item) }; const children = getBookLinkedSchema(item.children).tree; bindList(children); bindParent(children, linkedItem); return linkedItem; } function getLinkedItemList(start, end) { if (!start) { return []; } const result = []; let current = start; while (current && current !== end) { result.push(current); current = current.next; } if (end) { result.push(end); } return result; } function getLeafListLinkedItem(linkedItems) { const list = []; for (const item of linkedItems) { if (item.firstChild === null) { list.push(item); } else { list.push( ...getLeafListLinkedItem( getLinkedItemList(item.firstChild, item.lastChild) ) ); } } return list; } function getBookLinkedSchema(schema, root) { const linkedSchema = { start: null, end: null, tree: schema.filter(isNotEmpty).map(getBookLinkedItem) }; bindList(linkedSchema.tree); if (schema.length === 0 || !root) { return linkedSchema; } const leafList = getLeafListLinkedItem(linkedSchema.tree); bindLeafList(leafList); putPages(leafList); linkedSchema.start = leafList[0]; linkedSchema.end = leafList[leafList.length - 1]; linkedSchema.tree = getLinkedItemList(linkedSchema.tree[0], null); return linkedSchema; } function getLinkedLeafList(linkedSchema) { const list = []; let current = linkedSchema.start; while (current !== null) { list.push(current); current = current.nextLeaf; } return list; } function getSchemaFromLinkedList(linkedList) { const schema = []; for (const item of linkedList) { const { bookElementSchema, raw, firstChild, lastChild } = item; const { name, props } = bookElementSchema; if (name === "text" && raw && firstChild === null) { schema.push(raw); } else if (name === "text") { schema.push( ...getSchemaFromLinkedList(getLinkedItemList(firstChild, lastChild)) ); } else { schema.push({ name, props, children: getSchemaFromLinkedList( getLinkedItemList(firstChild, lastChild) ) }); } } return schema; } function putAfterItem(sourceItem, targetItem) { const { next, nextLeaf, parent } = sourceItem; targetItem.next = next; if (next) { next.previous = targetItem; } targetItem.nextLeaf = nextLeaf; if (nextLeaf) { nextLeaf.previousLeaf = targetItem; } sourceItem.next = targetItem; targetItem.previous = sourceItem; sourceItem.nextLeaf = targetItem; targetItem.previousLeaf = sourceItem; targetItem.parent = parent; if (parent && parent.lastChild === sourceItem) { parent.lastChild = targetItem; } } const PAGE_SIZE = 500; const NEW_LINE_SIZE = 10; const NEW_LINE_LONG_SIZE = 50; function putPages(items) { if (items.length === 0) { return; } let pageCount = 0; let currentBlockCount = PAGE_SIZE; for (const item of items) { const { raw, view } = item; const isBlock = view === "block"; let increment = 0; if (!isBlock && raw) { const newLines = raw.match(/\n\n*/g) ?? []; const newLineCount = newLines.reduce((s, e) => s + e.length, 0); const newLineSize = newLines.reduce( (s, e) => s + (e.length > 1 ? NEW_LINE_LONG_SIZE : NEW_LINE_SIZE), 0 ); const rawSize = raw.length - newLineCount; increment = rawSize + newLineSize; } currentBlockCount += increment; if (isBlock || currentBlockCount >= PAGE_SIZE) { currentBlockCount = 0; const pageItem = getPageLinkedItem(pageCount++); putAfterItem(item, pageItem); } } } function getStore({ builder, schema, externalBuilder, getBuild }) { const result = { dataByKeys: {}, elementsByKeys: getElementsByKeys(schema) }; const writeDataByKey = (key) => result.dataByKeys[key] = builder({ schema: [result.elementsByKeys[key]], store: result, externalBuilder, build: getBuild(result) }); const keys = Object.keys(result.elementsByKeys); for (let g = 0; g < 2; g++) { for (let i = 0; i < keys.length; i++) { writeDataByKey(keys[i]); } for (let i = keys.length - 1; i >= 0; i--) { writeDataByKey(keys[i]); } } return result; } function getElementsByKeys(schema) { let result = {}; for (const item of schema) { if (typeof item === "string") { continue; } result[item.props.key] = item; const childrenRecord = getElementsByKeys(item.children); result = { ...result, ...childrenRecord }; } return result; } const getIterableBook = iterableTreeGetter({ getChildren: (item) => typeof item !== "string" ? item.children : [], isLeaf: (item) => typeof item === "string" }); function getResources(schema) { const result = []; for (const item of getIterableBook(schema)) { if (typeof item === "string") continue; if (!isValidResource(item)) continue; result.push(item); } return result; } function isValidResource(resource) { if (resource.name !== "resource") return false; const { path, src } = resource.props; return Boolean(path) && Boolean(src); } function getResourceKey({ path, type }) { return `${type ? `${type}:` : ""}${path}`; } function getPathWithPrefix(src, prefix) { return `${prefix.replace(/\/$/, "")}/${src.replace(/^\.?\//, "")}`; } function getActualResourceMap({ schema, resourceOptions }) { const { prefix, pathMap, rawPrefix } = resourceOptions ?? {}; const resources = getResources(schema); const actualMap = /* @__PURE__ */ new Map(); for (const resource of resources) { const { path, src, type } = resource.props; const key = getResourceKey({ path, type }); let settings = { src }; const customSettings = pathMap == null ? void 0 : pathMap.get(key); if (customSettings) { settings = customSettings; } else if (prefix) { settings = { src: getPathWithPrefix(src, prefix) }; } else if (rawPrefix) { settings = { src: `${rawPrefix}${src}` }; } actualMap.set(key, settings); } return actualMap; } function calculateResources(schema, resourceMap = /* @__PURE__ */ new Map()) { var _a, _b; for (const item of schema) { if (typeof item === "string") continue; if ("src" in item.props && typeof item.props.src === "string" && item.props.src.startsWith("/")) { const localPath = item.props.src; const resourcePath = getResourceKey({ type: item.name, path: localPath }); const typeSrc = (_a = resourceMap.get(resourcePath)) == null ? void 0 : _a.src; const src = (_b = resourceMap.get(localPath)) == null ? void 0 : _b.src; if (!src && !typeSrc) { console.error(`Unknown resource ${item.props.src}`); } item.props.src = typeSrc ?? src ?? item.props.src; } calculateResources(item.children, resourceMap); } } function getInitial(type = "number", custom) { switch (type) { case "number": { if (custom === void 0) return 1; const n = typeof custom === "number" ? custom : parseFloat(custom); if (Number.isNaN(n)) return 1; return n; } case "char": { if (custom === void 0 || typeof custom !== "string" || custom.length !== 1) return "a".charCodeAt(0); return custom.charCodeAt(0); } case "latin": { return "a".charCodeAt(0); } case "big-latin": { return "A".charCodeAt(0); } case "roman": { return "ⅰ".charCodeAt(0); } case "big-roman": { return "Ⅰ".charCodeAt(0); } case "cyrillic": { return "а".charCodeAt(0); } case "big-cyrillic": { return "А".charCodeAt(0); } default: { return 1; } } } function calculateCounters(schema, counters = /* @__PURE__ */ new Map(), steps = /* @__PURE__ */ new Map(), bumped = /* @__PURE__ */ new Set()) { for (const item of schema) { if (typeof item === "string") { continue; } if (item.name !== "counter") { calculateCounters(item.children, counters, steps, bumped); continue; } const props = item.props; const fillCounter = (name, bump = false) => { const [value, isUnicodeChar] = counters.get(name) ?? [1, false]; let targetValue = value; if (bump && bumped.has(name)) { const step = steps.get(name) ?? 1; const newValue = value + step; counters.set(name, [newValue, isUnicodeChar]); targetValue = newValue; } item.children = [isUnicodeChar ? String.fromCharCode(targetValue) : `${targetValue}`]; }; if (props.start) { const type = props.type ?? "number"; const isUnicodeChar = type !== "number"; const initial = getInitial(type, props.initial); counters.set(props.start, [initial, isUnicodeChar]); if (props.step) { steps.set(props.start, props.step); } continue; } if (props.end) { counters.delete(props.end); steps.delete(props.end); bumped.delete(props.end); continue; } if (props.use) { fillCounter(props.use, true); bumped.add(props.use); continue; } if (props.last) { fillCounter(props.last); } } } function expandMarkers(schema) { const result = []; const stack = []; const getTarget = () => stack.length > 0 ? stack[stack.length - 1].children : result; const push = (item) => getTarget().push(item); for (const item of schema) { if (typeof item === "string") { push(item); } else if (item.marker === "start") { delete item.marker; stack.push(item); } else if (item.marker === "end") { const elem = stack.pop(); if (elem) push(elem); } else { item.children = expandMarkers(item.children); push(item); } } while (stack.length > 0) { const elem = stack.pop(); push(elem); } return result; } function mergeText(schema) { const result = []; let texts = []; const saveText = () => { if (texts.length > 0) { let last = ""; let chainText = ""; const chains = []; for (const text2 of texts) { if (last !== null && text2 === null) { chainText = last.endsWith("\n") ? last.slice(0, last.length - 1) : last; chains.push(chainText); } else if (last === null && text2 !== null && text2 !== "\n") { chainText = text2.startsWith("\n") ? text2.slice(1) : text2; chains.push(chainText); continue; } else if (last !== null && text2 !== null) { chains.push(last); } else if (last === null && text2 === "\n") { continue; } last = text2; } if (last !== null) chains.push(last); const text = chains.join("").replace(/\n\n\n\n+/g, "\n\n\n"); if (text !== "") result.push(text); texts = []; } }; for (const item of schema) { if (typeof item === "string") { if (item !== "") { if (texts.length > 0 && texts[texts.length - 1] !== null) { texts[texts.length - 1] += item; } else { texts.push(item); } } } else if (item.name === "hole") { texts.push(null); } else { saveText(); item.children = mergeText(item.children); result.push(item); } } saveText(); return result; } function addKeysToSchema(schema, keys = /* @__PURE__ */ new Map()) { const bumpKey = (name) => { keys.set(name, (keys.get(name) ?? 0) + 1); }; for (const item of schema) { if (typeof item === "string") { continue; } if (item.props.key === void 0) { bumpKey(item.name); item.props = { ...item.props, key: `${item.name}-${keys.get(item.name)}` }; } addKeysToSchema(item.children, keys); } } const prepareBookElem = { code: (item) => { if ((item.props.block || item.props.position !== "inline") && item.children.length === 1 && typeof item.children[0] === "string") { item.children[0] = item.children[0].trim(); } } }; function prepareSchema(schema) { for (const item of getIterableBook(schema)) { if (typeof item === "string") continue; const { name } = item; if (name in prepareBookElem) { prepareBookElem[name](item); } } } function createBook({ schema: originalSchema, builder: builderParams, externalBuilder, resourceOptions }) { let schema = JSON.parse(JSON.stringify(originalSchema)); schema = expandMarkers(schema); addKeysToSchema(schema); calculateCounters(schema); calculateResources(schema, getActualResourceMap({ schema, resourceOptions })); prepareSchema(schema); schema = mergeText(schema); const linkedSchema = getBookLinkedSchema(schema, true); const tokensSchema = getSchemaFromLinkedList(linkedSchema.tree); const builder = createBookBuilder(builderParams); const getBuild = (currentStore) => (localSchema) => builder({ schema: localSchema, store: currentStore, externalBuilder, build: getBuild(currentStore) }); const store = getStore({ builder, schema, externalBuilder, getBuild }); const build = getBuild(store); const meta = getBookMeta({ schema, store, builder, build }); schema = tokensSchema; schema = mergeText(schema); return { tokens: builder({ schema, store, externalBuilder, build }), meta, store }; } const synteticElementNameSet = /* @__PURE__ */ new Set(["text", "page", "error", "empty"]); const emptyElementNameSet = /* @__PURE__ */ new Set(["resource"]); function createBookBuilder({ elements, synteticElements }) { const builder = ({ schema, store, externalBuilder, parents = [], build }) => { const getItemBuilder = (store2) => (item) => { if (typeof item === "string") { const content = synteticElements.text({ raw: item, key: "" })({ children: [], store: store2, parents, build }); if (item.includes("Полнота системы связок")) { console.log({ item, content }); } return content; } let { name } = item; if (emptyElementNameSet.has(name) || item.props.hidden) { name = "empty"; } let targetElements = elements; if (synteticElementNameSet.has(name)) { targetElements = synteticElements; } let elemBuilder; if (name === "external") { if (externalBuilder) { const { scope = "custom", name: extName = "local" } = item.props; elemBuilder = getByPath([`${scope}`, `${extName}`], externalBuilder); } if (typeof elemBuilder !== "function") { elemBuilder = getByPath([name], elements); } } else { const path = name.split("."); elemBuilder = getByPath(path, targetElements); } const getError = (children, error) => { elemBuilder = synteticElements.error; return synteticElements.error({ props: item.props, name: item.name, error: error ?? "error" })({ children, store: store2, parents, build }); }; let childTokens = []; try { childTokens = builder({ schema: item.children, store: store2, externalBuilder, parents: [...parents, `${item.props.key}`], build }); } catch (e) { console.error(e); return getError([], String(e)); } if (typeof elemBuilder !== "function") { return getError(childTokens, "unknown element"); } try { return elemBuilder(item.props)({ children: childTokens, store: store2, parents, build }); } catch (e) { console.error(e); return getError([], String(e)); } }; const itemBuilder = getItemBuilder(store); return flatList(schema.map(itemBuilder)); }; return builder; } function createBookExternalBuilder({ element }) { return { externalBuilder: element }; } function getLayoutView(props) { if (props.inline) { return "inline"; } if (props.block) { return "block"; } return props.position === "inline" ? "inline" : "block"; } export { createBook, createBookBuilder, createBookExternalBuilder, defaultBlockNameList, defaultBlockNames, expandMarkers, flatList, getBookLinkedItem, getBookLinkedSchema, getBookMeta, getByPath, getElementView, getElementsByKeys, getIterableBook, getLayoutView, getLinkedLeafList, getPageLinkedItem, getPartialApply, getSchemaFromLinkedList, getStore, getTextLinkedItem, getTextTokens, isTemplateParams, iterableTreeGetter, mergeText, parseNewLines, print, rootBookHeader, templateToList };