@syfxlin/reks
Version:
Keystatic extensions library.
566 lines (553 loc) • 19.8 kB
JavaScript
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;