@litfamily/litgrok 1.0.13 → 1.0.15
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/.grok/hooks/auto-handoff-gate.mjs +15 -2
- package/.grok/rules/00-litgrok.md +1 -1
- package/.grok/skills/frontend-ui-ux/THIRD-PARTY-NOTICE.txt +0 -5
- package/.grok/skills/frontend-ui-ux/references/slop-register.md +4 -4
- package/.grok/skills/frontend-ui-ux/scripts/import-design-intelligence.mjs +1 -1
- package/.grok/skills/lit-docx/SKILL.md +59 -11
- package/.grok/skills/lit-docx/examples/01-report-ko.md +137 -0
- package/.grok/skills/lit-docx/examples/02-brief-ko.md +82 -0
- package/.grok/skills/lit-docx/examples/03-manual-ko.md +118 -0
- package/.grok/skills/lit-docx/examples/04-proposal-ko.md +92 -0
- package/.grok/skills/lit-docx/examples/05-memo-en.md +42 -0
- package/.grok/skills/lit-docx/examples/06-journal-en.md +90 -0
- package/.grok/skills/lit-docx/examples/07-report-en.md +103 -0
- package/.grok/skills/lit-docx/examples/08-proposal-en.md +90 -0
- package/.grok/skills/lit-docx/examples/09-memo-ko.md +41 -0
- package/.grok/skills/lit-docx/references/components.md +151 -0
- package/.grok/skills/lit-docx/references/direction-step.md +129 -0
- package/.grok/skills/lit-docx/references/page-composition.md +129 -0
- package/.grok/skills/lit-docx/references/tonalities/brief.md +98 -0
- package/.grok/skills/lit-docx/references/tonalities/journal.md +100 -0
- package/.grok/skills/lit-docx/references/tonalities/manual.md +107 -0
- package/.grok/skills/lit-docx/references/tonalities/memo.md +95 -0
- package/.grok/skills/lit-docx/references/tonalities/proposal.md +105 -0
- package/.grok/skills/lit-docx/references/tonalities/report.md +114 -0
- package/.grok/skills/lit-docx/scripts/convert_md_to_docx.py +394 -51
- package/.grok/skills/lit-docx/scripts/docx_design.py +2446 -0
- package/.grok/skills/lit-docx/scripts/docx_gate.py +203 -0
- package/.grok/skills/lit-docx/scripts/docx_layout.py +959 -0
- package/.grok/skills/lit-docx/scripts/run.mjs +3 -2
- package/.grok/skills/lit-docx/scripts/slop_lint.py +63 -17
- package/.grok/skills/lit-docx/templates/tonalities/brief.yaml +29 -0
- package/.grok/skills/lit-docx/templates/tonalities/journal.yaml +26 -0
- package/.grok/skills/lit-docx/templates/tonalities/manual.yaml +27 -0
- package/.grok/skills/lit-docx/templates/tonalities/memo.yaml +27 -0
- package/.grok/skills/lit-docx/templates/tonalities/proposal.yaml +27 -0
- package/.grok/skills/lit-docx/templates/tonalities/report.yaml +27 -0
- package/.grok/skills/lit-pptx/NOTICE.md +1 -1
- package/.grok/skills/lit-pptx/SKILL.md +70 -14
- package/.grok/skills/lit-pptx/agents/analyst.md +1 -1
- package/.grok/skills/lit-pptx/agents/thesis.md +2 -2
- package/.grok/skills/lit-pptx/examples/01-pitch-signal-ko.md +175 -0
- package/.grok/skills/lit-pptx/examples/02-business-review-ledger-ko.md +199 -0
- package/.grok/skills/lit-pptx/examples/03-research-talk-paper-en.md +177 -0
- package/.grok/skills/lit-pptx/examples/04-lecture-chalk-ko.md +176 -0
- package/.grok/skills/lit-pptx/examples/05-data-review-night-en.md +221 -0
- package/.grok/skills/lit-pptx/examples/06-image-led-atlas-en.md +143 -0
- package/.grok/skills/lit-pptx/examples/07-briefing-gazette-ko.md +192 -0
- package/.grok/skills/lit-pptx/examples/08-status-update-ledger-en.md +168 -0
- package/.grok/skills/lit-pptx/examples/09-pitch-studio-en.md +157 -0
- package/.grok/skills/lit-pptx/examples/10-image-led-studio-ko.md +175 -0
- package/.grok/skills/lit-pptx/examples/11-business-review-night-en.md +181 -0
- package/.grok/skills/lit-pptx/examples/12-status-update-gazette-ko.md +199 -0
- package/.grok/skills/lit-pptx/pretendard-font/public/static/Pretendard-Bold.otf +0 -0
- package/.grok/skills/lit-pptx/pretendard-font/public/static/Pretendard-Regular.otf +0 -0
- package/.grok/skills/lit-pptx/references/density-and-fill.md +155 -0
- package/.grok/skills/lit-pptx/references/direction-step.md +211 -0
- package/.grok/skills/lit-pptx/references/layout-families.md +193 -0
- package/.grok/skills/lit-pptx/references/title-treatments.md +134 -0
- package/.grok/skills/lit-pptx/references/tonalities/atlas.md +124 -0
- package/.grok/skills/lit-pptx/references/tonalities/chalk.md +135 -0
- package/.grok/skills/lit-pptx/references/tonalities/gazette.md +127 -0
- package/.grok/skills/lit-pptx/references/tonalities/ledger.md +134 -0
- package/.grok/skills/lit-pptx/references/tonalities/night.md +132 -0
- package/.grok/skills/lit-pptx/references/tonalities/paper.md +127 -0
- package/.grok/skills/lit-pptx/references/tonalities/signal.md +118 -0
- package/.grok/skills/lit-pptx/references/tonalities/studio.md +123 -0
- package/.grok/skills/lit-pptx/scripts/compile-deck.js +65 -17
- package/.grok/skills/lit-pptx/scripts/craft_extras.py +82 -18
- package/.grok/skills/lit-pptx/scripts/deck_output.py +598 -0
- package/.grok/skills/lit-pptx/scripts/embed_fonts.py +1 -1
- package/.grok/skills/lit-pptx/scripts/inventory.py +6 -2
- package/.grok/skills/lit-pptx/scripts/lib/directive-parser.js +2 -2
- package/.grok/skills/lit-pptx/scripts/lib/grid-resolver.js +521 -0
- package/.grok/skills/lit-pptx/scripts/lib/layout-resolver.js +401 -16
- package/.grok/skills/lit-pptx/scripts/lib/markdown-loader.js +6 -4
- package/.grok/skills/lit-pptx/scripts/lib/render-adapter-html.js +2 -2
- package/.grok/skills/lit-pptx/scripts/lib/render-adapter-pptx.js +500 -126
- package/.grok/skills/lit-pptx/scripts/lib/render-pack.js +3558 -0
- package/.grok/skills/lit-pptx/scripts/lib/slide-ast.js +54 -6
- package/.grok/skills/lit-pptx/scripts/lib/template-registry.js +335 -11
- package/.grok/skills/lit-pptx/scripts/qa_deck.py +43 -10
- package/.grok/skills/lit-pptx/scripts/run.mjs +3 -2
- package/.grok/skills/lit-pptx/scripts/validate_pptx.py +17 -5
- package/.grok/skills/lit-pptx/templates/boilerplate_professional.css +6 -6
- package/.grok/skills/lit-pptx/templates/enrolled/AZURE-A2Z/layout-mapping.yaml +1 -1
- package/.grok/skills/lit-pptx/templates/enrolled/AZURE-A2Z/template.yaml +4 -1
- package/.grok/skills/lit-pptx/templates/enrolled/AZURE-PRO/layout-mapping.yaml +1 -1
- package/.grok/skills/lit-pptx/templates/enrolled/AZURE-PRO/template.yaml +4 -1
- package/.grok/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +99 -0
- package/.grok/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +104 -0
- package/.grok/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +99 -0
- package/.grok/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +100 -0
- package/.grok/skills/lit-pptx/templates/tonalities/night/pack.yaml +100 -0
- package/.grok/skills/lit-pptx/templates/tonalities/paper/pack.yaml +105 -0
- package/.grok/skills/lit-pptx/templates/tonalities/signal/pack.yaml +101 -0
- package/.grok/skills/lit-pptx/templates/tonalities/studio/pack.yaml +99 -0
- package/.grok/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +1 -1
- package/.grok/skills/lit-typographic-motion/fonts/pins.json +6 -6
- package/.grok/skills/lit-typographic-motion/references/runtime.md +1 -1
- package/.grok/skills/lit-typographic-motion/references/style-bible.md +2 -2
- package/.grok/skills/lit-typographic-motion/references/type-craft.md +2 -2
- package/.grok/skills/lit-typographic-motion/scripts/lib/gate.mjs +1 -1
- package/.grok/skills/litgrok/SKILL.md +1 -1
- package/.grok-plugin/plugin.json +1 -1
- package/CHANGELOG.md +11 -0
- package/README.md +9 -9
- package/README_ko-KR.md +9 -9
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/reference.md +1 -1
- package/docs/reference_ko-KR.md +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/.grok/skills/lit-pptx/pretendard-font/public/static/PretendardGOV-Bold.otf +0 -0
- package/.grok/skills/lit-pptx/pretendard-font/public/static/PretendardGOV-Regular.otf +0 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""One pass/fail gate for a converted .docx and the Markdown it came from.
|
|
3
|
+
|
|
4
|
+
It reads five things and prints one JSON report (exit 0 = PASS, 1 = FAIL):
|
|
5
|
+
|
|
6
|
+
package the file is a readable ZIP with a document part, no NaN attribute values, and
|
|
7
|
+
python-docx reopens it
|
|
8
|
+
content the body has text, every Markdown heading reached the document, no unfilled blank
|
|
9
|
+
([제품명], XXX, TBD, ○○, {{ … }}) is left, and no frontmatter was printed as text
|
|
10
|
+
lint slop_lint's prose rules on the source (journal outline rules only for a manuscript)
|
|
11
|
+
and its DOCX audits: numeric-column alignment (OF-302), Hangul font pairing, and on
|
|
12
|
+
a publisher profile that profile's design rules
|
|
13
|
+
output docx_layout's checks: heading labels and order, the notice's colon, the restraint rules
|
|
14
|
+
of a tonality document, and with --layout the page checks on a LibreOffice render (fill,
|
|
15
|
+
spill onto a near-empty page, stranded headings, split short lists, a heading apart from
|
|
16
|
+
its table, table and figure splits, component variety, cover)
|
|
17
|
+
compare with --compare OTHER.docx, two tonalities of one source must differ in at least three
|
|
18
|
+
structural features
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
node .grok/skills/lit-docx/scripts/run.mjs docx_gate.py report.docx --source report.md --layout
|
|
22
|
+
node .grok/skills/lit-docx/scripts/run.mjs docx_gate.py paper.docx --source paper.md --publisher elsevier --kind manuscript
|
|
23
|
+
|
|
24
|
+
A PASS means no defect was found. Whether the pages read well is decided by looking at them.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import argparse
|
|
29
|
+
import json
|
|
30
|
+
import re
|
|
31
|
+
import shutil
|
|
32
|
+
import subprocess
|
|
33
|
+
import sys
|
|
34
|
+
import tempfile
|
|
35
|
+
import zipfile
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
from docx import Document
|
|
39
|
+
|
|
40
|
+
import docx_layout
|
|
41
|
+
import slop_lint
|
|
42
|
+
from convert_md_to_docx import parse_frontmatter
|
|
43
|
+
|
|
44
|
+
SKILL = Path(__file__).resolve().parents[1]
|
|
45
|
+
HEADING = re.compile(r"^(#{1,6})\s+(.+?)\s*#*\s*$")
|
|
46
|
+
# A bracket holding a digit is a citation marker ([1], [S3]); a blank holds none.
|
|
47
|
+
BLANK = re.compile(r"\[(?![^\]\n]*\d)[^\]\n]{1,24}\]|\bX{3,}\b|\bTBD\b|\bTODO\b|○○|OOO|\{\{[^}]*\}\}|_{4,}|_{2,}")
|
|
48
|
+
LEAK = re.compile(r"^(?:---|title:|subtitle:|author:|authors:|date:|tonality:)\s*", re.M)
|
|
49
|
+
# Audits every profile shares; the rest belong to a named publisher profile.
|
|
50
|
+
SHARED_DESIGN = {"rule-45-highlighted-text", "rule-46-tracked-changes", "rule-55-ellipsis"}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def check_package(path: Path) -> dict:
|
|
54
|
+
problems = []
|
|
55
|
+
try:
|
|
56
|
+
with zipfile.ZipFile(path) as archive:
|
|
57
|
+
names = set(archive.namelist())
|
|
58
|
+
if "word/document.xml" not in names or "[Content_Types].xml" not in names:
|
|
59
|
+
problems.append("document part or content types missing")
|
|
60
|
+
bad = archive.testzip()
|
|
61
|
+
if bad:
|
|
62
|
+
problems.append(f"corrupt member {bad}")
|
|
63
|
+
for name in names:
|
|
64
|
+
if name.endswith(".xml") and b'="NaN"' in archive.read(name):
|
|
65
|
+
problems.append(f"NaN attribute value in {name}")
|
|
66
|
+
Document(str(path))
|
|
67
|
+
except (zipfile.BadZipFile, KeyError, ValueError, OSError) as error:
|
|
68
|
+
problems.append(f"cannot open: {error}")
|
|
69
|
+
return {"pass": not problems, "problems": problems}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def source_headings(text: str) -> list[str]:
|
|
73
|
+
found, fenced = [], False
|
|
74
|
+
for line in text.splitlines():
|
|
75
|
+
if line.lstrip().startswith(("```", "~~~")):
|
|
76
|
+
fenced = not fenced
|
|
77
|
+
continue
|
|
78
|
+
match = None if fenced else HEADING.match(line)
|
|
79
|
+
if match:
|
|
80
|
+
found.append(re.sub(r"[*_`]", "", match.group(2)).strip())
|
|
81
|
+
return found
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def check_content(path: Path, markdown: str | None) -> dict:
|
|
85
|
+
document = Document(str(path))
|
|
86
|
+
paragraphs = [p.text.strip() for p in document.paragraphs if p.text.strip()]
|
|
87
|
+
cells = [cell.text for table in document.tables for row in table.rows for cell in row.cells]
|
|
88
|
+
blanks = sorted({m.group(0) for text in paragraphs + cells for m in BLANK.finditer(text)})
|
|
89
|
+
leaked = [p for p in paragraphs[:8] if LEAK.match(p)]
|
|
90
|
+
# A heading may stand inside a side-by-side columns table, so cells count as text too.
|
|
91
|
+
seen = docx_layout.norm("".join(paragraphs + cells))
|
|
92
|
+
missing = [h for h in source_headings(markdown)] if markdown else []
|
|
93
|
+
missing = [h for h in missing if docx_layout.norm(h) not in seen]
|
|
94
|
+
chars = sum(map(len, paragraphs)) + sum(map(len, cells))
|
|
95
|
+
return {"pass": chars > 0 and not blanks and not leaked and not missing, "characters": chars,
|
|
96
|
+
"blanks": blanks[:20], "frontmatter_leak": leaked[:4], "missing_headings": missing[:20]}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def lint(source: Path | None, path: Path, publisher: str | None, kind: str) -> dict:
|
|
100
|
+
registry = SKILL / "templates" / "registry.yaml"
|
|
101
|
+
profile = slop_lint.load_publisher(registry, publisher or "korean-generic")
|
|
102
|
+
rules = slop_lint.load_phrase_rules(SKILL / "references" / "slop_phrase_list.yaml")
|
|
103
|
+
prose, locale = [], "auto"
|
|
104
|
+
if source is not None:
|
|
105
|
+
genre = "manuscript" if kind == "manuscript" else "general"
|
|
106
|
+
prose, _front, _body, locale = slop_lint.lint_text(source.read_text(encoding="utf-8"), profile, rules, "auto", genre)
|
|
107
|
+
design = slop_lint.audit_docx_design(path, profile)
|
|
108
|
+
if publisher is None:
|
|
109
|
+
design = [f for f in design if f.rule_id in SHARED_DESIGN]
|
|
110
|
+
craft = slop_lint.audit_docx_craft(path)
|
|
111
|
+
if locale in {"ko", "mixed"} or source is None:
|
|
112
|
+
design += slop_lint.audit_docx_cjk(path)
|
|
113
|
+
row = lambda f: {"rule": f.rule_id, "severity": f.severity, "line": f.line, "message": f.message, "excerpt": f.excerpt[:80]} # noqa: E731
|
|
114
|
+
blocking = [row(f) for f in prose + design + craft if f.severity == "HIGH"]
|
|
115
|
+
return {"pass": not blocking, "locale": locale, "blocking": blocking[:40],
|
|
116
|
+
"advisories": [row(f) for f in prose + design + craft if f.severity != "HIGH"][:40]}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def render(path: Path, out: Path) -> Path | None:
|
|
120
|
+
"""PDF through LibreOffice with a private profile, so a running office session is never touched."""
|
|
121
|
+
soffice = shutil.which("soffice")
|
|
122
|
+
if not soffice:
|
|
123
|
+
return None
|
|
124
|
+
profile = out / "profile"
|
|
125
|
+
subprocess.run([soffice, f"-env:UserInstallation=file://{profile}", "--headless", "--convert-to", "pdf",
|
|
126
|
+
"--outdir", str(out), str(path)], capture_output=True, text=True, timeout=180)
|
|
127
|
+
pdf = out / f"{path.stem}.pdf"
|
|
128
|
+
return pdf if pdf.exists() else None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def output_checks(path: Path, source: Path | None, args) -> dict:
|
|
132
|
+
front = parse_frontmatter(source.read_text(encoding="utf-8"))[0] if source else {}
|
|
133
|
+
tonality = bool(args.tonality or front.get("tonality"))
|
|
134
|
+
info = docx_layout.read_docx(path)
|
|
135
|
+
found = docx_layout.structural(info, front, args.kind == "manuscript", tonality) + docx_layout.restraint(path, tonality)
|
|
136
|
+
result = {"components": info["components"], "rendered": False}
|
|
137
|
+
if args.compare:
|
|
138
|
+
mine, other = docx_layout.structure(path), docx_layout.structure(args.compare.expanduser().resolve())
|
|
139
|
+
differ = docx_layout.structure_diff(mine, other)
|
|
140
|
+
result["structure"] = {"differ": differ}
|
|
141
|
+
if len(differ) < 3:
|
|
142
|
+
found.append(docx_layout.finding("tonality.structure", "FAIL", None,
|
|
143
|
+
f"{len(differ)} structural difference(s): {', '.join(differ) or 'none'}",
|
|
144
|
+
"Tonalities differ in structure: title block, summary, numbering, components, running head, contents."))
|
|
145
|
+
if args.layout:
|
|
146
|
+
with tempfile.TemporaryDirectory(prefix="litgrok-docx-gate-") as tmp:
|
|
147
|
+
try:
|
|
148
|
+
pdf = render(path, Path(tmp))
|
|
149
|
+
except subprocess.TimeoutExpired:
|
|
150
|
+
pdf = None
|
|
151
|
+
if pdf is None:
|
|
152
|
+
result["render"] = "LibreOffice is unavailable or the render failed; the page checks did not run"
|
|
153
|
+
else:
|
|
154
|
+
paged, pages = docx_layout.paged(info, docx_layout.read_pdf(pdf), tonality, front)
|
|
155
|
+
found += paged
|
|
156
|
+
result.update(pages, rendered=True)
|
|
157
|
+
result["findings"] = [f for f in found if f["severity"] == "FAIL"]
|
|
158
|
+
result["advisories"] = [f for f in found if f["severity"] != "FAIL"]
|
|
159
|
+
return result
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def main(argv: list[str] | None = None) -> int:
|
|
163
|
+
parser = argparse.ArgumentParser(description="Pass/fail gate for a converted DOCX.")
|
|
164
|
+
parser.add_argument("docx", type=Path)
|
|
165
|
+
parser.add_argument("--source", type=Path, help="the Markdown the DOCX was converted from")
|
|
166
|
+
parser.add_argument("--publisher", help="the publisher profile used (korean-generic, elsevier, acs, ieee, nature); omit otherwise")
|
|
167
|
+
parser.add_argument("--kind", choices=("report", "manuscript"), default="report")
|
|
168
|
+
parser.add_argument("--tonality", help="the tonality used, when the source frontmatter does not name it")
|
|
169
|
+
parser.add_argument("--layout", action="store_true", help="render with LibreOffice and run the page checks")
|
|
170
|
+
parser.add_argument("--compare", type=Path, help="the same source in another tonality")
|
|
171
|
+
args = parser.parse_args(argv)
|
|
172
|
+
|
|
173
|
+
path = args.docx.expanduser().resolve()
|
|
174
|
+
source = args.source.expanduser().resolve() if args.source else None
|
|
175
|
+
package = check_package(path)
|
|
176
|
+
empty = {"pass": False}
|
|
177
|
+
content = check_content(path, source.read_text(encoding="utf-8") if source else None) if package["pass"] else empty
|
|
178
|
+
lint_report = lint(source, path, args.publisher, args.kind) if package["pass"] else empty
|
|
179
|
+
output = output_checks(path, source, args) if package["pass"] else {"findings": [], "advisories": []}
|
|
180
|
+
|
|
181
|
+
reasons = []
|
|
182
|
+
if not package["pass"]:
|
|
183
|
+
reasons.append("package: " + "; ".join(package["problems"][:5]))
|
|
184
|
+
if content.get("blanks"):
|
|
185
|
+
reasons.append(f"content: unfilled blanks {content['blanks'][:5]} — write realistic values and label them as examples")
|
|
186
|
+
if content.get("frontmatter_leak"):
|
|
187
|
+
reasons.append("content: frontmatter printed as body text")
|
|
188
|
+
if content.get("missing_headings"):
|
|
189
|
+
reasons.append(f"content: headings missing from the document {content['missing_headings'][:5]}")
|
|
190
|
+
if package["pass"] and not content.get("characters"):
|
|
191
|
+
reasons.append("content: the document has no text")
|
|
192
|
+
if lint_report.get("blocking"):
|
|
193
|
+
reasons.append("lint: " + ", ".join(sorted({f["rule"] for f in lint_report["blocking"]})))
|
|
194
|
+
if output["findings"]:
|
|
195
|
+
reasons.append("output: " + ", ".join(sorted({f["check"] for f in output["findings"]})))
|
|
196
|
+
report = {"file": str(path), "pass": not reasons, "failure_reasons": reasons, "profile": args.publisher or "plain",
|
|
197
|
+
"kind": args.kind, "package": package, "content": content, "lint": lint_report, "output": output}
|
|
198
|
+
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
199
|
+
return 0 if report["pass"] else 1
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
if __name__ == "__main__":
|
|
203
|
+
sys.exit(main())
|