ghostwriter-muse-pages 10.0.0 → 12.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build_pages.py +218 -9
- package/kit.js +280 -19
- package/package.json +1 -1
package/build_pages.py
CHANGED
|
@@ -1,29 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Build the book's pages (Overview, Book Setup, Voice, Stories, Notes, Outline, Manuscript, Ghostwriter).
|
|
2
|
+
"""Build the book's pages (Overview, Book Setup, Voice, Stories, Notes, Outline, Manuscript, Cover, Ghostwriter).
|
|
3
3
|
|
|
4
4
|
Reads <book>/Dashboard/book.json the status and outline, written by the ghostwriter
|
|
5
|
+
<book>/START-HERE.md only its "Method:" line, when book.json does not say which version is in use
|
|
5
6
|
<book>/2-Plan/01-book-setup.md the Book Setup page, read as it is
|
|
6
7
|
<book>/2-Plan/02-voice-profile.md the Voice page, with 05-style-sheet.md, read as they are
|
|
7
8
|
<book>/2-Plan/04-story-bank.md the Stories page, read as it is
|
|
8
9
|
<book>/2-Plan/00-notes.md the notes board, read as it is
|
|
9
10
|
<book>/5-Manuscript/*.md the chapters, read as they are
|
|
10
|
-
|
|
11
|
+
<book>/2-Plan/cover/cover.json the Cover page, with the picture it names if the author brought one
|
|
12
|
+
Writes <book>/Dashboard/Overview.html, Setup.html, Voice.html, Stories.html, Notes.html, Outline.html, Manuscript.html, Cover.html, Ghostwriter.html
|
|
13
|
+
<book>/Dashboard/cover-picture.* a copy of the author's own cover picture, for the pages in the folder
|
|
11
14
|
<book>/Dashboard/_live/ only with --live: book-dashboard.html to publish, plus data/*.json to load into it
|
|
12
15
|
|
|
13
16
|
Usage: python3 build_pages.py --book "/path/to/Book Folder" [--live]
|
|
14
17
|
python3 build_pages.py --version
|
|
15
|
-
Python 3 standard library only. This one file is the whole builder: the design of the pages is not in it.
|
|
18
|
+
Python 3 standard library only (a large cover picture is made smaller when the Pillow library happens to be installed). This one file is the whole builder: the design of the pages is not in it.
|
|
16
19
|
Every page it writes is a few lines that load the page code, at one exact version, from the address in KIT_URL.
|
|
17
20
|
"""
|
|
18
21
|
import argparse, json, os, re, sys
|
|
19
22
|
|
|
20
|
-
BUILDER = "0.
|
|
21
|
-
KIT_VERSION = "
|
|
23
|
+
BUILDER = "0.9.0" # this builder's own version
|
|
24
|
+
KIT_VERSION = "12.0.0" # the exact version of the page code every page loads
|
|
22
25
|
KIT_URL = "https://cdn.jsdelivr.net/npm/ghostwriter-muse-pages@" + KIT_VERSION + "/kit.js"
|
|
23
26
|
FONTS_URL = "https://fonts.googleapis.com/css2?family=Caveat:wght@600;700&family=Inter:wght@400;500;600&family=Lora:ital,wght@0,400;0,500;0,600;1,400&display=swap"
|
|
24
27
|
GHOSTWRITERS = {"lawrence", "kenji", "saira", "octavio", "zora", "boo", "imani", "indigo", "idris", "ryu", "marcus-aurelius", "frederick-douglass", "jane-austen",
|
|
25
28
|
"li-bai", "mary-shelley", "murasaki-shikibu", "phillis-wheatley", "rumi", "sor-juana", "william-shakespeare"}
|
|
26
|
-
PAGES = {"overview": "Overview.html", "setup": "Setup.html", "voice": "Voice.html", "stories": "Stories.html", "notes": "Notes.html", "outline": "Outline.html", "manuscript": "Manuscript.html", "ghostwriter": "Ghostwriter.html"}
|
|
29
|
+
PAGES = {"overview": "Overview.html", "setup": "Setup.html", "voice": "Voice.html", "stories": "Stories.html", "notes": "Notes.html", "outline": "Outline.html", "manuscript": "Manuscript.html", "cover": "Cover.html", "ghostwriter": "Ghostwriter.html"}
|
|
27
30
|
STAGE_COUNT = 10
|
|
28
31
|
PAGE_KIT = int(KIT_VERSION.split(".")[0]) # the page version written beside the live link in START-HERE.md
|
|
29
32
|
STORY_FIELDS = {"home": "home", "role": "role", "when": "when", "narrative": "narrative", "how it ended": "ended", "teaching point": "point", "people": "people",
|
|
@@ -36,6 +39,8 @@ WHEN = re.compile(r"^(worked out\s+|about\s+|around\s+|circa\s+|c\.\s*|~\s*)?(\d
|
|
|
36
39
|
STATUSES = {"done", "waiting", "current", "todo"}
|
|
37
40
|
PROPOSED = re.compile(r"\s*\(proposed[^)]*\)\s*\.?\s*$", re.I)
|
|
38
41
|
MARKER = re.compile(r"^\[(AUTHOR TO CONFIRM|STORY NEEDED):\s*(.*?)\]?$")
|
|
42
|
+
VERSION = re.compile(r"^\d+(\.\d+){1,2}$")
|
|
43
|
+
DAY = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
|
39
44
|
|
|
40
45
|
|
|
41
46
|
OLD_STAGES = []
|
|
@@ -371,6 +376,186 @@ def read_stories(plan, problems):
|
|
|
371
376
|
return {"status": status, "confirmed": confirmed, "items": items}
|
|
372
377
|
|
|
373
378
|
|
|
379
|
+
COVER_LAYOUTS = ("paper", "bold", "quiet")
|
|
380
|
+
COVER_FONTS = {"Fraunces", "Cormorant Garamond", "Playfair Display", "DM Serif Display", "Libre Baskerville", "Lora", "Archivo Black", "Archivo", "Bebas Neue", "Oswald", "Space Grotesk", "Inter"}
|
|
381
|
+
HEX = re.compile(r"^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$")
|
|
382
|
+
VIEWBOX = re.compile(r"^\s*-?\d+(?:\.\d+)?(?:[\s,]+-?\d+(?:\.\d+)?){3}\s*$")
|
|
383
|
+
COVER_ID = re.compile(r"^[a-z0-9][a-z0-9-]{0,40}$")
|
|
384
|
+
SVG_BAD = re.compile(r"<\s*(?:script|foreignObject|image|use|a|text|tspan|style|iframe|animate\w*|set|svg)\b|\bon[a-z]+\s*=|href\s*=|javascript:|<!|<\?", re.I)
|
|
385
|
+
PART = 180000 # letters of a picture carried by one document on the live page
|
|
386
|
+
PICTURE_MAX = 1500000 # bytes: the largest picture the pages carry
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def picture_kind(data):
|
|
390
|
+
if data[:3] == b"\xff\xd8\xff":
|
|
391
|
+
return "image/jpeg", "jpg"
|
|
392
|
+
if data[:8] == b"\x89PNG\r\n\x1a\n":
|
|
393
|
+
return "image/png", "png"
|
|
394
|
+
if data[:4] == b"RIFF" and data[8:12] == b"WEBP":
|
|
395
|
+
return "image/webp", "webp"
|
|
396
|
+
if data[:4] == b"GIF8":
|
|
397
|
+
return "image/gif", "gif"
|
|
398
|
+
return None, None
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def shrink(data, mime):
|
|
402
|
+
"""Make a large picture smaller, if this computer happens to have the Pillow library. Without it the picture is used as it is."""
|
|
403
|
+
if mime == "image/gif":
|
|
404
|
+
return data, mime
|
|
405
|
+
try:
|
|
406
|
+
import io
|
|
407
|
+
from PIL import Image, ImageOps
|
|
408
|
+
im = Image.open(io.BytesIO(data))
|
|
409
|
+
if max(im.size) <= 1500 and len(data) <= 350000:
|
|
410
|
+
return data, mime
|
|
411
|
+
im = ImageOps.exif_transpose(im)
|
|
412
|
+
im.thumbnail((1500, 1500))
|
|
413
|
+
buf = io.BytesIO()
|
|
414
|
+
im.convert("RGB").save(buf, "JPEG", quality=86, optimize=True)
|
|
415
|
+
return (buf.getvalue(), "image/jpeg") if len(buf.getvalue()) < len(data) else (data, mime)
|
|
416
|
+
except Exception:
|
|
417
|
+
return data, mime
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def cover_lettering(src, where, problems):
|
|
421
|
+
"""The typefaces, colors, and line breaks of one cover. Anything not on the lists is left out, and the page uses its own choice."""
|
|
422
|
+
out = {}
|
|
423
|
+
for k in ("titleFont", "textFont"):
|
|
424
|
+
if src.get(k):
|
|
425
|
+
if src[k] in COVER_FONTS:
|
|
426
|
+
out[k] = src[k]
|
|
427
|
+
else:
|
|
428
|
+
problems.append('cover.json: %s names the typeface "%s", which the pages do not carry. It is left out. Use one of: %s.' % (where, src[k], ", ".join(sorted(COVER_FONTS))))
|
|
429
|
+
if isinstance(src.get("titleLines"), list) and 1 <= len(src["titleLines"]) <= 6:
|
|
430
|
+
out["titleLines"] = [str(x) for x in src["titleLines"]]
|
|
431
|
+
if isinstance(src.get("caps"), bool):
|
|
432
|
+
out["caps"] = src["caps"]
|
|
433
|
+
return out
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def read_cover(book_dir, dash, problems):
|
|
437
|
+
"""The Cover page is built from 2-Plan/cover/cover.json, written by the ghostwriter, and from the picture it names, if the
|
|
438
|
+
author brought one. Returns (the cover as the folder pages carry it, the cover as the live page carries it, the picture's parts)."""
|
|
439
|
+
path = os.path.join(book_dir, "2-Plan", "cover", "cover.json")
|
|
440
|
+
if not os.path.exists(path):
|
|
441
|
+
return None, None, []
|
|
442
|
+
try:
|
|
443
|
+
raw = json.load(open(path, encoding="utf-8"))
|
|
444
|
+
except (ValueError, UnicodeDecodeError) as e:
|
|
445
|
+
problems.append("2-Plan/cover/cover.json is not valid JSON (%s), so the Cover page is not built. Fix the file and build again." % e)
|
|
446
|
+
return None, None, []
|
|
447
|
+
if not isinstance(raw, dict):
|
|
448
|
+
problems.append("2-Plan/cover/cover.json must be one object. See the cover reference.")
|
|
449
|
+
return None, None, []
|
|
450
|
+
out, seen = {"concepts": []}, set()
|
|
451
|
+
for c in raw.get("concepts") or []:
|
|
452
|
+
if not isinstance(c, dict) or not COVER_ID.match(str(c.get("id", ""))) or c.get("id") == "own" or not str(c.get("name", "")).strip():
|
|
453
|
+
problems.append('cover.json: each direction needs an id (small letters, digits, hyphens; not "own") and a name. One was left out.')
|
|
454
|
+
continue
|
|
455
|
+
if c["id"] in seen:
|
|
456
|
+
problems.append('cover.json: two directions share the id "%s". The second one was left out.' % c["id"])
|
|
457
|
+
continue
|
|
458
|
+
if len(out["concepts"]) == 4:
|
|
459
|
+
problems.append("cover.json: the Cover page shows at most four directions. The rest were left out.")
|
|
460
|
+
break
|
|
461
|
+
seen.add(c["id"])
|
|
462
|
+
where = 'the direction "%s"' % c["name"]
|
|
463
|
+
d = {"id": c["id"], "name": str(c["name"]).strip(), "why": str(c.get("why", "")).strip(), "layout": c.get("layout") if c.get("layout") in COVER_LAYOUTS else "paper"}
|
|
464
|
+
if c.get("layout") not in COVER_LAYOUTS:
|
|
465
|
+
problems.append("cover.json: %s has no layout, or one the pages do not know. It is drawn as \"paper\". Use one of: %s." % (where, ", ".join(COVER_LAYOUTS)))
|
|
466
|
+
colors = {k: v for k, v in (c.get("colors") or {}).items() if k in ("paper", "ink", "accent") and HEX.match(str(v))} if isinstance(c.get("colors"), dict) else {}
|
|
467
|
+
if len(colors) < 3:
|
|
468
|
+
problems.append("cover.json: %s needs three colors, paper, ink, and accent, each written like #1F3B2D. The page fills in the missing ones." % where)
|
|
469
|
+
d["colors"] = colors
|
|
470
|
+
d.update(cover_lettering(c, where, problems))
|
|
471
|
+
m = c.get("motif")
|
|
472
|
+
if isinstance(m, dict) and str(m.get("svg", "")).strip():
|
|
473
|
+
svg = str(m["svg"]).strip()
|
|
474
|
+
if len(svg) > 20000 or SVG_BAD.search(svg) or not VIEWBOX.match(str(m.get("viewBox", ""))):
|
|
475
|
+
problems.append("cover.json: the drawing for %s was left out. A drawing is plain shapes only (path, rect, circle, ellipse, line, polygon, polyline, g, and gradients), at most 20,000 letters, with a viewBox of four numbers, and no words, pictures, links, or scripts." % where)
|
|
476
|
+
else:
|
|
477
|
+
d["motif"] = {"viewBox": str(m["viewBox"]).strip(), "svg": svg}
|
|
478
|
+
out["concepts"].append(d)
|
|
479
|
+
out["prompts"] = [{"label": str(x.get("label", "")).strip(), "why": str(x.get("why", "")).strip(), "say": str(x["say"]).strip()}
|
|
480
|
+
for x in (raw.get("prompts") or []) if isinstance(x, dict) and str(x.get("say", "")).strip()][:6]
|
|
481
|
+
b = raw.get("back")
|
|
482
|
+
if isinstance(b, dict) and (b.get("headline") or b.get("text")):
|
|
483
|
+
text = b.get("text") if isinstance(b.get("text"), list) else [b.get("text")] if b.get("text") else []
|
|
484
|
+
status = str(b.get("status", "")).strip()
|
|
485
|
+
d = re.search(r"\d{4}-\d{2}-\d{2}", status)
|
|
486
|
+
out["back"] = {"status": "confirmed" if status.upper().startswith("CONFIRMED") else "draft", "confirmed": d.group(0) if d else "",
|
|
487
|
+
"headline": str(b.get("headline", "")).strip(), "text": [str(x).strip() for x in text if str(x).strip()][:8], "bio": str(b.get("bio", "")).strip()}
|
|
488
|
+
live, parts, own = dict(out), [], raw.get("own")
|
|
489
|
+
if isinstance(own, dict) and str(own.get("file", "")).strip():
|
|
490
|
+
rel = str(own["file"]).strip().replace("\\", "/")
|
|
491
|
+
src = os.path.normpath(os.path.join(book_dir, rel))
|
|
492
|
+
data = None
|
|
493
|
+
if not src.startswith(book_dir + os.sep) or not os.path.isfile(src):
|
|
494
|
+
problems.append('cover.json: the picture "%s" is not in the book folder, so the author\'s own cover is not shown. Give its place inside the book folder, for example 2-Plan/cover/my-cover.jpg.' % rel)
|
|
495
|
+
else:
|
|
496
|
+
data = open(src, "rb").read()
|
|
497
|
+
mime, ext = picture_kind(data)
|
|
498
|
+
if not mime:
|
|
499
|
+
problems.append('cover.json: "%s" is not a JPEG, PNG, WebP, or GIF picture, so it is not shown. Ask the author for one of those, or save a copy as JPEG.' % rel)
|
|
500
|
+
data = None
|
|
501
|
+
else:
|
|
502
|
+
data, mime = shrink(data, mime)
|
|
503
|
+
ext = picture_kind(data)[1]
|
|
504
|
+
if len(data) > PICTURE_MAX:
|
|
505
|
+
problems.append('cover.json: the picture "%s" is %.1f MB, which is more than the pages can carry (1.5 MB). Save a smaller copy beside it (1,500 pixels tall is plenty, as a JPEG), name that copy in own.file, and build again.' % (rel, len(data) / 1e6))
|
|
506
|
+
data = None
|
|
507
|
+
if data is not None:
|
|
508
|
+
import base64, hashlib
|
|
509
|
+
name = "cover-picture." + ext
|
|
510
|
+
open(os.path.join(dash, name), "wb").write(data)
|
|
511
|
+
b64, sig = base64.b64encode(data).decode("ascii"), hashlib.sha256(data).hexdigest()[:12]
|
|
512
|
+
chunks = [b64[i:i + PART] for i in range(0, len(b64), PART)]
|
|
513
|
+
parts = [{"sig": sig, "i": i, "of": len(chunks), "data": ch} for i, ch in enumerate(chunks)]
|
|
514
|
+
base = {"words": own.get("words") is not False, "note": str(own.get("note", "")).strip()}
|
|
515
|
+
if not base["words"]:
|
|
516
|
+
base["lettering"] = cover_lettering(own.get("lettering") if isinstance(own.get("lettering"), dict) else {}, "the author's own picture", problems)
|
|
517
|
+
ink = (own.get("lettering") or {}).get("ink") if isinstance(own.get("lettering"), dict) else None
|
|
518
|
+
if HEX.match(str(ink or "")):
|
|
519
|
+
base["lettering"]["ink"] = ink
|
|
520
|
+
out["own"] = dict(base, src=name)
|
|
521
|
+
live["own"] = dict(base, sig=sig, parts=len(chunks), type=mime, bytes=len(data))
|
|
522
|
+
chosen = str(raw.get("chosen", "") or "").strip()
|
|
523
|
+
if chosen and not (chosen in seen or (chosen == "own" and "own" in out)):
|
|
524
|
+
problems.append('cover.json: chosen is "%s", which is not one of the directions%s. No cover is marked as chosen.' % (chosen, "" if "own" in out else ", and there is no picture of the author's own"))
|
|
525
|
+
chosen = ""
|
|
526
|
+
out["chosen"] = live["chosen"] = chosen
|
|
527
|
+
return out, live, parts
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def read_version(book_dir, state, problems):
|
|
531
|
+
"""The version marker in the top bar of every page. The ghostwriter writes state.version at each refresh:
|
|
532
|
+
{"method": "0.8.1", "plugin": "0.8.0", "checked": "2026-10-10"}. Anything that is not a plain version or date is dropped.
|
|
533
|
+
When the method version is missing, it is read from the Method line of START-HERE.md."""
|
|
534
|
+
given = state.get("version") if isinstance(state.get("version"), dict) else {}
|
|
535
|
+
out = {}
|
|
536
|
+
for k in ("method", "plugin"):
|
|
537
|
+
if VERSION.match(str(given.get(k, "")).strip()):
|
|
538
|
+
out[k] = str(given[k]).strip()
|
|
539
|
+
if DAY.match(str(given.get("checked", "")).strip()):
|
|
540
|
+
out["checked"] = str(given["checked"]).strip()
|
|
541
|
+
if "method" not in out:
|
|
542
|
+
try:
|
|
543
|
+
for line in open(os.path.join(book_dir, "START-HERE.md"), encoding="utf-8"):
|
|
544
|
+
m = re.match(r"^\s*Method:\s*(\d+(?:\.\d+){1,2})\b", line)
|
|
545
|
+
if m:
|
|
546
|
+
out["method"] = m.group(1)
|
|
547
|
+
break
|
|
548
|
+
except (OSError, UnicodeDecodeError):
|
|
549
|
+
pass
|
|
550
|
+
problems.append('book.json has no state.version, so the version marker at the top of the pages %s. Write state.version at every refresh (see the pages reference).'
|
|
551
|
+
% ('shows the Method line of START-HERE.md (%s) and no date' % out["method"] if "method" in out else "is left off"))
|
|
552
|
+
elif "checked" not in out:
|
|
553
|
+
problems.append("state.version has no checked date (YYYY-MM-DD), so the pages cannot say when the version was last confirmed.")
|
|
554
|
+
out["builder"], out["pages"] = BUILDER, KIT_VERSION
|
|
555
|
+
state["version"] = out
|
|
556
|
+
return out
|
|
557
|
+
|
|
558
|
+
|
|
374
559
|
def untag(text):
|
|
375
560
|
"""Take a closing '(proposed)' or '(proposed, please check)' off a value, so the author reads a clean line."""
|
|
376
561
|
return PROPOSED.sub("", text or "").strip()
|
|
@@ -420,15 +605,17 @@ def main():
|
|
|
420
605
|
board = read_notes(os.path.join(book_dir, "2-Plan", "00-notes.md"), notes)
|
|
421
606
|
plan = os.path.join(book_dir, "2-Plan")
|
|
422
607
|
setup, voice, stories = read_setup(plan), read_voice(plan), read_stories(plan, notes)
|
|
608
|
+
cover, cover_live, cover_parts = read_cover(book_dir, dash, notes)
|
|
423
609
|
if OLD_STAGES:
|
|
424
610
|
notes.append("book.json lists 11 steps. The Proposal is no longer a step: write state.stages with 10 values and state.stage from 1 to 10 (see references/pages.md). This build dropped the sixth value for you.")
|
|
425
611
|
|
|
426
612
|
if state.get("ghostwriter") not in GHOSTWRITERS:
|
|
427
613
|
state["ghostwriter"] = "lawrence"
|
|
614
|
+
version = read_version(book_dir, state, notes)
|
|
428
615
|
title = state["title"].replace("&", "&").replace("<", "").replace(">", "")
|
|
429
616
|
|
|
430
617
|
# 1. The pages for the book folder: one small file per page, carrying the book's data with it.
|
|
431
|
-
snap = {"state": state, "outline": outline, "manuscript": manuscript, "notes": board, "setup": setup, "voice": voice, "stories": stories}
|
|
618
|
+
snap = {"state": state, "outline": outline, "manuscript": manuscript, "notes": board, "setup": setup, "voice": voice, "stories": stories, "cover": cover}
|
|
432
619
|
for key, fname in PAGES.items():
|
|
433
620
|
doc = ('<!doctype html>\n<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">'
|
|
434
621
|
"<style>img{max-width:100%}[hidden]{display:none!important}</style></head><body>\n"
|
|
@@ -438,7 +625,7 @@ def main():
|
|
|
438
625
|
# 2. The live page and its data, for sessions that can publish it.
|
|
439
626
|
live = os.path.join(dash, "_live")
|
|
440
627
|
if a.live:
|
|
441
|
-
write_live(live, title, state, outline, manuscript, board, setup, voice, stories)
|
|
628
|
+
write_live(live, title, state, outline, manuscript, board, setup, voice, stories, cover_live, cover_parts)
|
|
442
629
|
|
|
443
630
|
# 3. Say what was built, in numbers the ghostwriter can check against the folder.
|
|
444
631
|
total = 0
|
|
@@ -447,6 +634,8 @@ def main():
|
|
|
447
634
|
state["stage"], state["ghostwriter"], len(state.get("prompts", [])),
|
|
448
635
|
"" if state.get("prompts") else " (none written: the Overview falls back to the older next-step fields)",
|
|
449
636
|
", live files in Dashboard/_live" if a.live else ""))
|
|
637
|
+
print(" version shown at the top of the pages: %s%s%s" % (
|
|
638
|
+
version.get("method", "none"), (", plugin " + version["plugin"]) if "plugin" in version else "", (", the latest as of " + version["checked"]) if "checked" in version else ""))
|
|
450
639
|
for c in (outline or {}).get("chapters", []):
|
|
451
640
|
ch = manuscript.get(c["key"])
|
|
452
641
|
w = sum(s["words"] for s in ch["sections"]) if ch else 0
|
|
@@ -461,11 +650,20 @@ def main():
|
|
|
461
650
|
("%d lines, %s" % (len(setup["items"]), setup["status"])) if setup else "not written yet",
|
|
462
651
|
("%d lines, %s" % (len(voice["items"]) + len((voice.get("style") or {}).get("items", [])), voice["status"])) if voice else "not written yet",
|
|
463
652
|
("%d, %s" % (len(stories["items"]), stories["status"])) if stories else "not written yet"))
|
|
653
|
+
if cover is None:
|
|
654
|
+
print(" cover: not started")
|
|
655
|
+
else:
|
|
656
|
+
own = (cover_live or {}).get("own")
|
|
657
|
+
names = {c["id"]: c["name"] for c in cover["concepts"]}
|
|
658
|
+
print(" cover: %d direction%s (%d with a drawing); the author's own picture: %s; chosen: %s" % (
|
|
659
|
+
len(cover["concepts"]), "" if len(cover["concepts"]) == 1 else "s", sum(1 for c in cover["concepts"] if "motif" in c),
|
|
660
|
+
("%d KB, carried as coverimg/p-00%s" % (round(own["bytes"] / 1000), " to p-%02d" % (own["parts"] - 1) if own["parts"] > 1 else "")) if own else "none",
|
|
661
|
+
"the author's own picture" if cover["chosen"] == "own" else names.get(cover["chosen"], "nothing yet")))
|
|
464
662
|
for n in notes:
|
|
465
663
|
print("NOTE " + n)
|
|
466
664
|
|
|
467
665
|
|
|
468
|
-
def write_live(live, title, state, outline, manuscript, board, setup, voice, stories):
|
|
666
|
+
def write_live(live, title, state, outline, manuscript, board, setup, voice, stories, cover=None, cover_parts=()):
|
|
469
667
|
os.makedirs(os.path.join(live, "data", "manuscript"), exist_ok=True)
|
|
470
668
|
open(os.path.join(live, "book-dashboard.html"), "w", encoding="utf-8").write(shell(title + " Dashboard"))
|
|
471
669
|
json.dump(state, open(os.path.join(live, "data", "state.json"), "w", encoding="utf-8"), ensure_ascii=False, indent=1)
|
|
@@ -479,6 +677,17 @@ def write_live(live, title, state, outline, manuscript, board, setup, voice, sto
|
|
|
479
677
|
json.dump(body, open(os.path.join(live, "data", "notes", note["id"] + ".json"), "w", encoding="utf-8"), ensure_ascii=False, indent=1)
|
|
480
678
|
for key, ch in manuscript.items():
|
|
481
679
|
json.dump(ch, open(os.path.join(live, "data", "manuscript", key + ".json"), "w", encoding="utf-8"), ensure_ascii=False, indent=1)
|
|
680
|
+
if cover is not None:
|
|
681
|
+
json.dump(cover, open(os.path.join(live, "data", "cover.json"), "w", encoding="utf-8"), ensure_ascii=False, indent=1)
|
|
682
|
+
cdir = os.path.join(live, "data", "coverimg")
|
|
683
|
+
if cover_parts or os.path.isdir(cdir):
|
|
684
|
+
os.makedirs(cdir, exist_ok=True)
|
|
685
|
+
for part in cover_parts:
|
|
686
|
+
json.dump(part, open(os.path.join(cdir, "p-%02d.json" % part["i"]), "w", encoding="utf-8"))
|
|
687
|
+
for name in sorted(os.listdir(cdir)): # parts left from an earlier, larger picture are emptied: nothing in the folder is ever deleted
|
|
688
|
+
m = re.match(r"^p-(\d\d)\.json$", name)
|
|
689
|
+
if m and int(m.group(1)) >= len(cover_parts):
|
|
690
|
+
json.dump({"sig": "", "i": int(m.group(1)), "of": 0, "data": ""}, open(os.path.join(cdir, name), "w", encoding="utf-8"))
|
|
482
691
|
|
|
483
692
|
|
|
484
693
|
if __name__ == "__main__":
|