@lark-apaas/coding-steering 0.1.31 → 0.1.32-beta.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/package.json +1 -1
- package/steering/design-html/skills/pptx-style-extract/SKILL.md +112 -0
- package/steering/design-html/skills/pptx-style-extract/font-fallback.yaml +129 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/census.py +955 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/check_v2.py +907 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +945 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/export_consumer_md.py +75 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/export_consumer_zip.py +175 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +765 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +699 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/package.py +1120 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +461 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/query.py +562 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +679 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_font.py +68 -0
- package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +193 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Export a markdown-only consumer attachment for generation agents.
|
|
3
|
+
|
|
4
|
+
The full style package can still be zipped for audit/storage. This exporter is
|
|
5
|
+
for the runtime generation path: one plain markdown file with design.md and
|
|
6
|
+
layouts.md inlined, so attachment preprocessing does not enter archive handling.
|
|
7
|
+
"""
|
|
8
|
+
import argparse
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def fail(msg):
|
|
14
|
+
print('export_consumer_md.py: %s' % msg, file=sys.stderr)
|
|
15
|
+
sys.exit(1)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def read_text(path, required=True):
|
|
19
|
+
if not os.path.exists(path):
|
|
20
|
+
if required:
|
|
21
|
+
fail('missing %s' % path)
|
|
22
|
+
return ''
|
|
23
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
24
|
+
return f.read()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main():
|
|
28
|
+
ap = argparse.ArgumentParser()
|
|
29
|
+
ap.add_argument('pack_dir')
|
|
30
|
+
ap.add_argument('out_md')
|
|
31
|
+
args = ap.parse_args()
|
|
32
|
+
|
|
33
|
+
pack = os.path.abspath(args.pack_dir)
|
|
34
|
+
out = os.path.abspath(args.out_md)
|
|
35
|
+
if not os.path.isdir(pack):
|
|
36
|
+
fail('pack_dir not found: %s' % pack)
|
|
37
|
+
|
|
38
|
+
design = read_text(os.path.join(pack, 'design.md'))
|
|
39
|
+
layouts = read_text(os.path.join(pack, 'layouts.md'), required=False)
|
|
40
|
+
manifest = read_text(os.path.join(pack, 'manifest.json'), required=False)
|
|
41
|
+
|
|
42
|
+
content = [
|
|
43
|
+
'# Miaoda PPT Style Consumer Attachment',
|
|
44
|
+
'',
|
|
45
|
+
'> Runtime rule: this is a plain markdown style attachment. Use the text',
|
|
46
|
+
'> below directly. Do not invoke archive/zip preprocessing, do not search',
|
|
47
|
+
'> `.agent/*/attachments`, do not repair files, and do not read any other',
|
|
48
|
+
'> style package files. Start building after this document is read once.',
|
|
49
|
+
'',
|
|
50
|
+
'## design.md',
|
|
51
|
+
'',
|
|
52
|
+
design.rstrip(),
|
|
53
|
+
]
|
|
54
|
+
if layouts:
|
|
55
|
+
content += ['', '## layouts.md', '', layouts.rstrip()]
|
|
56
|
+
if manifest:
|
|
57
|
+
content += [
|
|
58
|
+
'',
|
|
59
|
+
'## manifest.json',
|
|
60
|
+
'',
|
|
61
|
+
'```json',
|
|
62
|
+
manifest.rstrip(),
|
|
63
|
+
'```',
|
|
64
|
+
]
|
|
65
|
+
content.append('')
|
|
66
|
+
|
|
67
|
+
os.makedirs(os.path.dirname(out), exist_ok=True)
|
|
68
|
+
with open(out, 'w', encoding='utf-8') as f:
|
|
69
|
+
f.write('\n'.join(content))
|
|
70
|
+
|
|
71
|
+
print('markdown -> %s (%d bytes)' % (out, os.path.getsize(out)))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == '__main__':
|
|
75
|
+
main()
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Export a consumer-only style package zip.
|
|
3
|
+
|
|
4
|
+
The zip keeps only files needed by the generation agent. Every entry is stored
|
|
5
|
+
without compression and the first entry is a plain-text guide containing the
|
|
6
|
+
fast path plus the core markdown, so attachment text extraction can surface the
|
|
7
|
+
rules without asking the agent to repair or parse binary zip bytes.
|
|
8
|
+
"""
|
|
9
|
+
import argparse
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import shutil
|
|
14
|
+
import sys
|
|
15
|
+
import zipfile
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
KEEP_ROOT = {'design.md', 'layouts.md', 'manifest.json'}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def fail(msg):
|
|
22
|
+
print('export_consumer_zip.py: %s' % msg, file=sys.stderr)
|
|
23
|
+
sys.exit(1)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def sha256_file(path):
|
|
27
|
+
h = hashlib.sha256()
|
|
28
|
+
with open(path, 'rb') as f:
|
|
29
|
+
for chunk in iter(lambda: f.read(1024 * 1024), b''):
|
|
30
|
+
h.update(chunk)
|
|
31
|
+
return h.hexdigest()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def copy_tree(src, dst, include_assets=True):
|
|
35
|
+
if os.path.exists(dst):
|
|
36
|
+
shutil.rmtree(dst)
|
|
37
|
+
os.makedirs(dst)
|
|
38
|
+
for name in KEEP_ROOT:
|
|
39
|
+
src_path = os.path.join(src, name)
|
|
40
|
+
if os.path.exists(src_path):
|
|
41
|
+
shutil.copy2(src_path, os.path.join(dst, name))
|
|
42
|
+
assets_src = os.path.join(src, 'assets')
|
|
43
|
+
if include_assets and os.path.isdir(assets_src):
|
|
44
|
+
shutil.copytree(assets_src, os.path.join(dst, 'assets'))
|
|
45
|
+
if not os.path.exists(os.path.join(dst, 'design.md')):
|
|
46
|
+
fail('missing design.md in %s' % src)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def rel_files(root):
|
|
50
|
+
out = []
|
|
51
|
+
for base, dirs, files in os.walk(root):
|
|
52
|
+
dirs[:] = sorted(d for d in dirs if d != 'ref')
|
|
53
|
+
for name in sorted(files):
|
|
54
|
+
path = os.path.join(base, name)
|
|
55
|
+
rel = os.path.relpath(path, root)
|
|
56
|
+
out.append(rel)
|
|
57
|
+
return out
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def rewrite_manifest(root):
|
|
61
|
+
manifest_path = os.path.join(root, 'manifest.json')
|
|
62
|
+
if not os.path.exists(manifest_path):
|
|
63
|
+
return
|
|
64
|
+
with open(manifest_path, 'r', encoding='utf-8') as f:
|
|
65
|
+
manifest = json.load(f)
|
|
66
|
+
files = []
|
|
67
|
+
for rel in rel_files(root):
|
|
68
|
+
if rel == 'manifest.json':
|
|
69
|
+
continue
|
|
70
|
+
path = os.path.join(root, rel)
|
|
71
|
+
files.append({
|
|
72
|
+
'path': rel,
|
|
73
|
+
'bytes': os.path.getsize(path),
|
|
74
|
+
'sha256': sha256_file(path),
|
|
75
|
+
})
|
|
76
|
+
digest = hashlib.sha256()
|
|
77
|
+
for item in files:
|
|
78
|
+
digest.update(item['path'].encode('utf-8'))
|
|
79
|
+
digest.update(item['sha256'].encode('ascii'))
|
|
80
|
+
manifest['files'] = files
|
|
81
|
+
manifest['totals'] = {
|
|
82
|
+
'file_count': len(files),
|
|
83
|
+
'bytes': sum(x['bytes'] for x in files),
|
|
84
|
+
'sha256': digest.hexdigest(),
|
|
85
|
+
'asset_count': len(manifest.get('assets') or []),
|
|
86
|
+
}
|
|
87
|
+
manifest['consumerOnly'] = True
|
|
88
|
+
manifest['ref'] = None
|
|
89
|
+
with open(manifest_path, 'w', encoding='utf-8') as f:
|
|
90
|
+
json.dump(manifest, f, ensure_ascii=False, indent=2)
|
|
91
|
+
f.write('\n')
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def extract_fast_path(design):
|
|
95
|
+
lines = design.splitlines()
|
|
96
|
+
start = None
|
|
97
|
+
end = None
|
|
98
|
+
for i, line in enumerate(lines):
|
|
99
|
+
if line.strip() == '## Agent Fast Path':
|
|
100
|
+
start = i
|
|
101
|
+
continue
|
|
102
|
+
if start is not None and i > start and line.startswith('## '):
|
|
103
|
+
end = i
|
|
104
|
+
break
|
|
105
|
+
if start is None:
|
|
106
|
+
return ''
|
|
107
|
+
return '\n'.join(lines[start:end]) + '\n'
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def build_prefix(root):
|
|
111
|
+
with open(os.path.join(root, 'design.md'), 'r', encoding='utf-8') as f:
|
|
112
|
+
design = f.read()
|
|
113
|
+
layouts_path = os.path.join(root, 'layouts.md')
|
|
114
|
+
layouts = ''
|
|
115
|
+
if os.path.exists(layouts_path):
|
|
116
|
+
with open(layouts_path, 'r', encoding='utf-8') as f:
|
|
117
|
+
layouts = f.read()
|
|
118
|
+
fast = extract_fast_path(design)
|
|
119
|
+
layout_excerpt = layouts[:12000]
|
|
120
|
+
if len(layouts) > len(layout_excerpt):
|
|
121
|
+
layout_excerpt += '\n<!-- layouts.md truncated in text prefix; full file is in zip. -->\n'
|
|
122
|
+
return '''PPTX_STYLE_PACKAGE_TEXT_ENTRY_V1
|
|
123
|
+
|
|
124
|
+
This attachment is a Miaoda PPT style package. Use the markdown below directly.
|
|
125
|
+
Do not repair ZIP bytes, parse binary payloads, search attachment folders, or
|
|
126
|
+
rebuild the archive. The ZIP payload after this text exists only to carry image
|
|
127
|
+
assets. If you can read this text, you already have the style rules.
|
|
128
|
+
|
|
129
|
+
=== 00_AGENT_FAST_PATH.md ===
|
|
130
|
+
%s
|
|
131
|
+
=== design.md ===
|
|
132
|
+
%s
|
|
133
|
+
=== layouts.md ===
|
|
134
|
+
%s
|
|
135
|
+
=== ZIP_PAYLOAD_BELOW ===
|
|
136
|
+
''' % (fast, design, layout_excerpt)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def write_zip(root, zip_payload_path):
|
|
140
|
+
prefix = os.path.basename(root)
|
|
141
|
+
compression = zipfile.ZIP_STORED
|
|
142
|
+
with zipfile.ZipFile(zip_payload_path, 'w', compression=compression) as zf:
|
|
143
|
+
guide = build_prefix(root)
|
|
144
|
+
zf.writestr(prefix + '/00_AGENT_FAST_PATH.md', guide)
|
|
145
|
+
for rel in rel_files(root):
|
|
146
|
+
zf.write(os.path.join(root, rel), prefix + '/' + rel)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def main():
|
|
150
|
+
ap = argparse.ArgumentParser()
|
|
151
|
+
ap.add_argument('pack_dir')
|
|
152
|
+
ap.add_argument('out_zip')
|
|
153
|
+
ap.add_argument('--work-dir',
|
|
154
|
+
help='consumer-only directory to materialize; default: <out_zip without .zip>')
|
|
155
|
+
ap.add_argument('--no-assets', action='store_true',
|
|
156
|
+
help='export text-only zip: keep asset metadata but omit binary assets')
|
|
157
|
+
args = ap.parse_args()
|
|
158
|
+
|
|
159
|
+
pack = os.path.abspath(args.pack_dir)
|
|
160
|
+
out_zip = os.path.abspath(args.out_zip)
|
|
161
|
+
if not os.path.isdir(pack):
|
|
162
|
+
fail('pack_dir not found: %s' % pack)
|
|
163
|
+
work_dir = os.path.abspath(args.work_dir or os.path.splitext(out_zip)[0])
|
|
164
|
+
copy_tree(pack, work_dir, include_assets=not args.no_assets)
|
|
165
|
+
rewrite_manifest(work_dir)
|
|
166
|
+
|
|
167
|
+
write_zip(work_dir, out_zip)
|
|
168
|
+
|
|
169
|
+
print('consumer dir -> %s' % work_dir)
|
|
170
|
+
print('zip -> %s (%d bytes)' % (out_zip, os.path.getsize(out_zip)))
|
|
171
|
+
print('files -> %d' % len(rel_files(work_dir)))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
if __name__ == '__main__':
|
|
175
|
+
main()
|