Ctrl K
ComunidadGuías…

src/deck.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { WeaveError } from './engine.js';
import { create } from './vendor/decklet/create.mjs';
import { validate } from './vendor/decklet/validate.mjs';

export const DECK = Object.freeze({
  slides: 'Slides',
  model: 'Model',
  chrome: 'Chrome',
  style: 'Style',
  order: 'Order',
  space: 'Space',
  format: 'Format',
  layout: 'Layout',
  version: 'Version',
  key: 'Key',
  supersedes: 'Supersedes',
});

const fieldsOf = (db) => Object.values(db.fields ?? {});
const named = (db, name) => fieldsOf(db).find((f) => f.name === name);

const isSlideTable = (db) => named(db, DECK.model)?.type === 'document';

export function deckRole(db, tables = null) {
  if (!db) return null;
  const slides = named(db, DECK.slides);
  if (slides?.type === 'relation' && slides.config?.many
    && (!tables || isSlideTable(tables[slides.config.targetDb]))) return 'deck';
  if (isSlideTable(db)) return 'slide';
  return null;
}

export function parseJsonDoc(text) {
  const raw = String(text ?? '').trim();
  if (!raw) return null;
  const fenced = raw.match(/```(?:json|js|javascript)?\s*\n([\s\S]*?)\n?```/);
  return JSON.parse(fenced ? fenced[1] : raw);
}

const errorSlide = (label, message) => ({
  name: label,
  els: [
    { role: 'H1', x: 60, y: 190, w: 840, text: `${label} could not be read` },
    { role: 'Body', x: 60, y: 250, w: 840, color: 'var(--muted)', text: String(message).slice(0, 240) },
  ],
});

const placeholderSlide = (title) => ({
  name: title,
  els: [
    { role: 'H1', x: 60, y: 190, w: 840, text: title },
    { role: 'Body', x: 60, y: 250, w: 840, color: 'var(--muted)', text: 'Link slides to this deck and they compose here, in link order.' },
  ],
});

function slideFrom(weave, summary, warnings) {
  const ent = weave.readEntity(summary.id);
  const label = `${ent.db.split('/').pop()}#${ent.publicId}`;
  let parsed;
  try {
    parsed = parseJsonDoc(ent.docs?.[DECK.model]);
  } catch (err) {
    warnings.push(`${label} — ${DECK.model} is not JSON: ${err.message}`);
    return errorSlide(label, err.message);
  }
  if (parsed == null) {
    warnings.push(`${label} — ${DECK.model} is empty`);
    return errorSlide(label, `${DECK.model} is empty`);
  }
  const slide = Array.isArray(parsed) ? { els: parsed } : { ...parsed };
  if (!Array.isArray(slide.els)) {
    warnings.push(`${label} — no els array in ${DECK.model}`);
    return errorSlide(label, 'the model has no els array');
  }
  slide.name = slide.name ?? ent.name ?? label;
  const layout = ent.fields?.[DECK.layout];
  if (layout) slide.layout = typeof layout === 'object' ? layout.name : String(layout);
  return slide;
}

function ordered(summaries, order) {
  const list = String(order ?? '').split(/[,\n]+/).map((s) => s.trim()).filter(Boolean);
  if (!list.length) return summaries;
  const picked = [];
  for (const ref of list) {
    const bare = ref.replace(/^.*#/, '');
    const hit = summaries.find((s) => s.id === ref || String(s.publicId) === bare
      || s.name?.toLowerCase() === ref.toLowerCase());
    if (hit && !picked.includes(hit)) picked.push(hit);
  }
  return picked.length ? picked : summaries;
}

const docJson = (ent, field, warnings) => {
  try {
    return parseJsonDoc(ent.docs?.[field]) ?? null;
  } catch (err) {
    warnings.push(`${field} is not JSON: ${err.message}`);
    return null;
  }
};

const plainValue = (v) => (v && typeof v === 'object' ? (v.name ?? null) : v);

export function composeDeckModel(weave, ref, { admit = () => true } = {}) {
  const ent = weave.readEntity(ref);
  const db = weave.state.tables[ent.dbId];
  if (deckRole(db, weave.state.tables) !== 'deck') {
    throw new WeaveError(`'${ent.db}#${ent.publicId}' is not a deck — a deck table has a many-relation named '${DECK.slides}'`, 'invalid');
  }
  const warnings = [];
  const chrome = docJson(ent, DECK.chrome, warnings) ?? {};
  const style = docJson(ent, DECK.style, warnings);
  const linked = (ent.fields?.[DECK.slides] ?? []).filter((s) => admit(s.id));
  const picked = ordered(linked, ent.fields?.[DECK.order]);
  const slides = picked.map((s) => slideFrom(weave, s, warnings));
  if (!slides.length) {
    warnings.push('this deck has no slides yet');
    slides.push(placeholderSlide(ent.name || 'Empty deck'));
  }
  const model = { ...chrome, title: ent.name || 'deck', slides };
  return {
    model, style, warnings, entity: ent,
    options: {
      title: ent.name || undefined,
      format: plainValue(ent.fields?.[DECK.format]) || undefined,
      space: plainValue(ent.fields?.[DECK.space]) || undefined,
    },
  };
}

export function composeSlideModel(weave, ref, { admit = () => true } = {}) {
  const ent = weave.readEntity(ref);
  const db = weave.state.tables[ent.dbId];
  if (deckRole(db, weave.state.tables) !== 'slide') {
    throw new WeaveError(`'${ent.db}#${ent.publicId}' is not a slide — a slide table has a document field named '${DECK.model}'`, 'invalid');
  }
  const warnings = [];
  const parent = firstDeckOf(weave, ent, admit);
  const context = parent ? composeDeckModel(weave, parent.id, { admit }) : null;
  const slide = slideFrom(weave, { id: ent.id }, warnings);
  const model = context
    ? { ...context.model, title: ent.name || context.model.title, slides: [slide] }
    : { title: ent.name || 'slide', slides: [slide] };
  return {
    model, style: context?.style ?? null, warnings, entity: ent,
    options: { ...(context?.options ?? {}), title: ent.name || undefined },
  };
}

function firstDeckOf(weave, ent, admit) {
  const db = weave.state.tables[ent.dbId];
  for (const f of fieldsOf(db)) {
    if (f.type !== 'relation') continue;
    if (deckRole(weave.state.tables[f.config?.targetDb], weave.state.tables) !== 'deck') continue;
    const val = ent.fields?.[f.name];
    const first = (Array.isArray(val) ? val : [val]).find((d) => d && admit(d.id));
    if (first) return first;
  }
  return null;
}

export function renderDeck(weave, ref, { template, admit = () => true } = {}) {
  const ent = weave.readEntity(ref);
  const role = deckRole(weave.state.tables[ent.dbId], weave.state.tables);
  const composed = role === 'slide' ? composeSlideModel(weave, ref, { admit }) : composeDeckModel(weave, ref, { admit });
  const { html, deck } = create(composed.model, { ...composed.options, style: composed.style, template });
  const v = validate(deck);
  return { html, model: deck, errors: v.errors, warnings: [...composed.warnings, ...v.warnings] };
}

export function newSlideVersion(weave, ref, { promote = false } = {}) {
  const ent = weave.readEntity(ref);
  const db = weave.state.tables[ent.dbId];
  if (deckRole(db, weave.state.tables) !== 'slide') {
    throw new WeaveError(`'${ent.db}#${ent.publicId}' is not a slide — versioning is for slide tables (a document field named '${DECK.model}')`, 'invalid');
  }
  if (!named(db, DECK.version)) {
    throw new WeaveError(`'${ent.db}' has no '${DECK.version}' field — a slide table versions on ${DECK.version} + ${DECK.key}`, 'invalid');
  }
  const values = {};
  for (const f of fieldsOf(db)) {
    if (f.system || ['lookup', 'rollup', 'formula', 'document', 'attachments', 'avatar'].includes(f.type)) continue;
    if (f.name === DECK.version || f.name === DECK.supersedes) continue;
    if (f.type === 'relation' && f.config?.many) continue;
    const raw = ent.raw?.[f.name];
    const val = f.type === 'relation' ? (Array.isArray(raw) ? raw[0] : raw) : raw;
    if (val != null && val !== '') values[f.name] = val;
  }
  const current = Number(ent.fields?.[DECK.version] ?? 1);
  values[DECK.version] = (Number.isFinite(current) ? current : 1) + 1;
  if (named(db, DECK.supersedes)?.type === 'relation') values[DECK.supersedes] = ent.id;
  const docs = {};
  for (const [name, text] of Object.entries(ent.docs ?? {})) if (text) docs[name] = text;
  const next = weave.createEntity(db.id, { values, docs });
  if (promote) {
    for (const f of fieldsOf(db)) {
      if (f.type !== 'relation' || !f.config?.many) continue;
      if (deckRole(weave.state.tables[f.config?.targetDb], weave.state.tables) !== 'deck') continue;
      for (const deckRow of ent.fields?.[f.name] ?? []) {
        const holder = weave.readEntity(deckRow.id);
        const list = (holder.fields?.[DECK.slides] ?? []).map((s) => (s.id === ent.id ? next.id : s.id));
        weave.updateEntity(deckRow.id, { [DECK.slides]: list });
      }
    }
  }
  return next;
}