UNPKG

@syfxlin/reks

Version:

Keystatic extensions library.

566 lines (553 loc) 19.8 kB
Object.defineProperty(exports, '__esModule', { value: true }); var path = require('path'); var Markdoc = require('@markdoc/markdoc'); var core = require('@keystatic/core'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var path__default = /*#__PURE__*/_interopDefault(path); var Markdoc__default = /*#__PURE__*/_interopDefault(Markdoc); const decoder$1 = new TextDecoder(); function toUTF8String(input, start = 0, end = input.length) { return decoder$1.decode(input.slice(start, end)); } function toHexString(input, start = 0, end = input.length) { return input.slice(start, end).reduce((memo, i)=>memo + `0${i.toString(16)}`.slice(-2), ""); } function readInt16LE(input, offset = 0) { const val = input[offset] + input[offset + 1] * 2 ** 8; return val | (val & 2 ** 15) * 0x1FFFE; } const readUInt16BE = (input, offset = 0)=>input[offset] * 2 ** 8 + input[offset + 1]; const readUInt16LE = (input, offset = 0)=>input[offset] + input[offset + 1] * 2 ** 8; function readUInt24LE(input, offset = 0) { return input[offset] + input[offset + 1] * 2 ** 8 + input[offset + 2] * 2 ** 16; } function readUInt32BE(input, offset = 0) { return input[offset] * 2 ** 24 + input[offset + 1] * 2 ** 16 + input[offset + 2] * 2 ** 8 + input[offset + 3]; } function readUInt32LE(input, offset = 0) { return input[offset] + input[offset + 1] * 2 ** 8 + input[offset + 2] * 2 ** 16 + input[offset + 3] * 2 ** 24; } function readUInt(input, bits, offset, isBigEndian) { offset = offset || 0; if (isBigEndian) { if (bits === 16) { return readUInt16BE(input, offset); } else { return readUInt32BE(input, offset); } } else { if (bits === 16) { return readUInt16LE(input, offset); } else { return readUInt32LE(input, offset); } } } // From: https://github.com/image-size/image-size/blob/main/lib/types/jpg.ts const EXIF_MARKER = "45786966"; const APP1_DATA_SIZE_BYTES = 2; const EXIF_HEADER_BYTES = 6; const TIFF_BYTE_ALIGN_BYTES = 2; const BIG_ENDIAN_BYTE_ALIGN = "4d4d"; const LITTLE_ENDIAN_BYTE_ALIGN = "4949"; // Each entry is exactly 12 bytes const IDF_ENTRY_BYTES = 12; const NUM_DIRECTORY_ENTRIES_BYTES = 2; function isEXIF(input) { return toHexString(input, 2, 6) === EXIF_MARKER; } function extractSize(input, index) { return { height: readUInt16BE(input, index), width: readUInt16BE(input, index + 2) }; } function extractOrientation(exifBlock, isBigEndian) { // let STATIC_MOTOROLA_TIFF_HEADER_BYTES = 2 // let TIFF_IMAGE_FILE_DIRECTORY_BYTES = 4 const idfOffset = 8; // IDF osset works from right after the header bytes // (so the offset includes the tiff byte align) const offset = EXIF_HEADER_BYTES + idfOffset; const idfDirectoryEntries = readUInt(exifBlock, 16, offset, isBigEndian); for(let directoryEntryNumber = 0; directoryEntryNumber < idfDirectoryEntries; directoryEntryNumber++){ const start = offset + NUM_DIRECTORY_ENTRIES_BYTES + directoryEntryNumber * IDF_ENTRY_BYTES; const end = start + IDF_ENTRY_BYTES; // Skip on corrupt EXIF blocks if (start > exifBlock.length) { return; } const block = exifBlock.slice(start, end); const tagNumber = readUInt(block, 16, 0, isBigEndian); // 0x0112 (decimal: 274) is the `orientation` tag ID if (tagNumber === 274) { const dataFormat = readUInt(block, 16, 2, isBigEndian); if (dataFormat !== 3) { return; } // unsinged int has 2 bytes per component // if there would more than 4 bytes in total it's a pointer const numberOfComponents = readUInt(block, 32, 4, isBigEndian); if (numberOfComponents !== 1) { return; } return readUInt(block, 16, 8, isBigEndian); } } } function validateExifBlock(input, index) { // Skip APP1 Data Size const exifBlock = input.slice(APP1_DATA_SIZE_BYTES, index); // Consider byte alignment const byteAlign = toHexString(exifBlock, EXIF_HEADER_BYTES, EXIF_HEADER_BYTES + TIFF_BYTE_ALIGN_BYTES); // Ignore Empty EXIF. Validate byte alignment const isBigEndian = byteAlign === BIG_ENDIAN_BYTE_ALIGN; const isLittleEndian = byteAlign === LITTLE_ENDIAN_BYTE_ALIGN; if (isBigEndian || isLittleEndian) { return extractOrientation(exifBlock, isBigEndian); } } function validateInput(input, index) { // index should be within buffer limits if (index > input.length) { throw new TypeError("Corrupt JPG, exceeded buffer limits"); } // Every JPEG block must begin with a 0xFF if (input[index] !== 0xFF) { throw new TypeError("Invalid JPG, marker table corrupted"); } } const JPG = { validate (input) { return toHexString(input, 0, 2) === "ffd8"; }, calculate (input) { // Skip 4 chars, they are for signature input = input.slice(4); let orientation; let next; while(input.length){ // read length of the next block const i = readUInt16BE(input, 0); if (isEXIF(input)) { orientation = validateExifBlock(input, i); } // ensure correct format validateInput(input, i); // 0xFFC0 is baseline standard(SOF) // 0xFFC1 is baseline optimized(SOF) // 0xFFC2 is progressive(SOF2) next = input[i + 1]; if (next === 0xC0 || next === 0xC1 || next === 0xC2) { const size = extractSize(input, i + 5); if (!orientation) { return size; } return { height: size.height, orientation, width: size.width }; } // move to the next block input = input.slice(i + 2); } throw new TypeError("Invalid JPG, no size found"); } }; // From: https://github.com/image-size/image-size/blob/main/lib/types/png.ts const pngSignature = "PNG\r\n\x1A\n"; const pngImageHeaderChunkName = "IHDR"; // Used to detect "fried" png's: http://www.jongware.com/pngdefry.html const pngFriedChunkName = "CgBI"; const PNG = { validate (input) { if (pngSignature === toUTF8String(input, 1, 8)) { let chunkName = toUTF8String(input, 12, 16); if (chunkName === pngFriedChunkName) { chunkName = toUTF8String(input, 28, 32); } if (chunkName !== pngImageHeaderChunkName) { throw new TypeError("Invalid PNG"); } return true; } return false; }, calculate (input) { if (toUTF8String(input, 12, 16) === pngFriedChunkName) { return { height: readUInt32BE(input, 36), width: readUInt32BE(input, 32) }; } return { height: readUInt32BE(input, 20), width: readUInt32BE(input, 16) }; } }; // From: https://github.com/image-size/image-size/blob/main/lib/types/webp.ts function calculateExtended(input) { return { height: 1 + readUInt24LE(input, 7), width: 1 + readUInt24LE(input, 4) }; } function calculateLossless(input) { return { height: 1 + ((input[4] & 0xF) << 10 | input[3] << 2 | (input[2] & 0xC0) >> 6), width: 1 + ((input[2] & 0x3F) << 8 | input[1]) }; } function calculateLossy(input) { // `& 0x3fff` returns the last 14 bits // TO-DO: include webp scaling in the calculations return { height: readInt16LE(input, 8) & 0x3FFF, width: readInt16LE(input, 6) & 0x3FFF }; } const WEBP = { validate (input) { const riffHeader = toUTF8String(input, 0, 4) === "RIFF"; const webpHeader = toUTF8String(input, 8, 12) === "WEBP"; const vp8Header = toUTF8String(input, 12, 15) === "VP8"; return riffHeader && webpHeader && vp8Header; }, calculate (input) { const chunkHeader = toUTF8String(input, 12, 16); input = input.slice(20, 30); // Extended webp stream signature if (chunkHeader === "VP8X") { const extendedHeader = input[0]; const validStart = (extendedHeader & 0xC0) === 0; const validEnd = (extendedHeader & 0x01) === 0; if (validStart && validEnd) { return calculateExtended(input); } else { // TODO: breaking change throw new TypeError("Invalid WebP"); } } // Lossless webp stream signature if (chunkHeader === "VP8 " && input[0] !== 0x2F) { return calculateLossy(input); } // Lossy webp stream signature const signature = toHexString(input, 3, 6); if (chunkHeader === "VP8L" && signature !== "9d012a") { return calculateLossless(input); } throw new TypeError("Invalid WebP"); } }; // From: https://github.com/image-size/image-size/blob/main/lib/types/gif.ts const gifRegexp = /^GIF8[79]a/; const GIF = { validate (input) { return gifRegexp.test(toUTF8String(input, 0, 6)); }, calculate (input) { return { height: readUInt16LE(input, 8), width: readUInt16LE(input, 6) }; } }; function imageSize(input) { for (const type of [ JPG, PNG, WEBP, GIF ]){ if (type.validate(input)) { return type.calculate(input); } } throw new TypeError("Invalid Image, unsupported format"); } const encoder = new TextEncoder(); const decoder = new TextDecoder(); function visit(node, files) { if (node.type === "image") { const info = path__default.default.posix.parse(node.attributes.src ?? ""); if (info.name && !/\.\d+x\d+$/.test(info.name)) { for (const file of files){ const base = info.base; const image = file.get(base); if (image) { const size = imageSize(image); const name = `${info.name}.${size.width}x${size.height}`; node.attributes.src = path__default.default.posix.format({ name, dir: info.dir, ext: info.ext }); file.delete(base); file.set(`${name}${info.ext}`, image); break; } } } } for (const item of node.children){ visit(item, files); } } function append(name, size) { const info = path__default.default.posix.parse(name); if (/\.\d+x\d+$/.test(info.name)) { return name; } else { const name = `${info.name}.${size.width}x${size.height}`; const dir = info.dir; const ext = info.ext; return path__default.default.posix.format({ name, dir, ext }); } } const fields = { ...core.fields, date (config) { const result = core.fields.date(config); const serialize = result.serialize; result.serialize = function(value) { const serialized = serialize(value); if (typeof config.updatingValue === "string") { const date = new Date(config.updatingValue); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return { value: `${year}-${month}-${day}` }; } if (config.updatingValue?.kind === "now") { const date = new Date(); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return { value: `${year}-${month}-${day}` }; } return serialized; }; return result; }, datetime (config) { const result = core.fields.datetime(config); const serialize = result.serialize; result.serialize = function(value) { const serialized = serialize(value); if (typeof config.updatingValue === "string") { const date = new Date(config.updatingValue); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); const hour = String(date.getHours()).padStart(2, "0"); const minute = String(date.getMinutes()).padStart(2, "0"); return { value: `${year}-${month}-${day}T${hour}:${minute}` }; } if (config.updatingValue?.kind === "now") { const date = new Date(); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); const hour = String(date.getHours()).padStart(2, "0"); const minute = String(date.getMinutes()).padStart(2, "0"); return { value: `${year}-${month}-${day}T${hour}:${minute}` }; } return serialized; }; return result; }, image (config) { const result = core.fields.image(config); const serialize = result.serialize; result.serialize = function(value, extra) { const serialized = serialize(value, extra); if (serialized.asset) { const size = imageSize(serialized.asset.content); serialized.value = append(serialized.value, size); serialized.asset.filename = append(serialized.asset.filename, size); } return serialized; }; return result; }, document (config) { if (config.formatting === undefined) { config.formatting = { alignment: true, softBreaks: true, inlineMarks: { bold: true, italic: true, underline: true, strikethrough: true, code: true, superscript: true, subscript: true, keyboard: true }, listTypes: { ordered: true, unordered: true }, headingLevels: { levels: [ 1, 2, 3, 4, 5, 6 ] }, blockTypes: { blockquote: true, code: true } }; } if (config.links === undefined) { config.links = true; } if (config.dividers === undefined) { config.dividers = true; } if (config.tables === undefined) { config.tables = true; } if (config.layouts === undefined) { config.layouts = [ [ 1 ], [ 1, 1 ], [ 1, 1, 1 ], [ 1, 1, 1, 1 ] ]; } const result = core.fields.document(config); const serialize = result.serialize; result.serialize = function(value, extra) { const serialized = serialize(value, extra); if (serialized.content) { const node = Markdoc__default.default.parse(decoder.decode(serialized.content)); // @ts-expect-error visit(node, [ serialized.other, ...serialized.external.values() ]); serialized.content = encoder.encode(Markdoc__default.default.format(node)); } return serialized; }; return result; } }; const utils = { document (body) { const visit = (items, names)=>{ const headings = []; const contents = []; for (const item of items){ if (item.text) { contents.push(item.text); } else if (item.type === "heading") { const results = visit(item.children, names); item.name = results.contents.join(""); item.slug = `${item.name}${names[item.name] ? `-${names[item.name]}` : ``}`; item.link = `#${encodeURIComponent(item.slug)}`; headings.push({ name: item.name, slug: item.slug, link: item.link, level: item.level }); headings.push(...results.headings); contents.push(...results.contents); names[item.name] = (names[item.name] ?? 0) + 1; } else if (item.children) { const results = visit(item.children, names); headings.push(...results.headings); contents.push(...results.contents); } } return { headings, contents }; }; const build = (headings, parent = { level: 0, children: [] })=>{ parent.children = parent.children ?? []; while(headings.length){ const heading = headings.shift(); if (heading.level > parent.level) { parent.children.push(build(headings, heading)); } else { headings.unshift(heading); return parent; } } return parent; }; const results = visit(body, {}); const document = body; const headings = build(results.headings.map((h, i)=>({ ...h, step: i }))).children; const contents = results.contents.join(" "); const excerpts = contents.length <= 140 ? contents : `${contents.substring(0, 140)}...`; return { document, headings, contents, excerpts }; }, pagination (size, items) { if (size <= 0) { throw new Error(`size must be greater than zero.`); } const pages = []; for(let i = 0; i < Math.ceil(items.length / size); i++){ pages.push(items.slice(i * size, i * size + size)); } return { page: (index)=>pages[index - 1], items: pages, pages: pages.length, total: items.length }; } }; exports.fields = fields; exports.utils = utils;