designlang 12.10.0 → 12.11.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +45 -0
- package/bin/design-extract.js +110 -29
- package/package.json +4 -3
- package/src/formatters/brand-book.js +2 -1
- package/src/pdf.js +56 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"name": "designlang",
|
|
10
10
|
"source": "./",
|
|
11
11
|
"description": "Eight slash commands wrapping the designlang CLI: /extract (full design language \u2192 DTCG, Tailwind, Figma), /grade (shareable HTML report card + SVG badge), /battle (head-to-head graded comparison), /remix (restyle in 6 vocabularies \u2014 brutalist, swiss, art-deco, cyberpunk, soft-ui, editorial), /pack (one downloadable design-system bundle), /theme-swap (OKLCH-correct recolour around a new brand primary), /brand (full editorial brand-guidelines book \u2014 13 chapters, hand-off-ready), /pair (fuse two designs across configurable axes \u2014 colours from one site, typography from another).",
|
|
12
|
-
"version": "12.10.
|
|
12
|
+
"version": "12.10.1",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "Manavarya Singh"
|
|
15
15
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "designlang",
|
|
3
3
|
"description": "Extract any website's design language and ship it. Eight slash commands \u2014 /extract, /grade, /battle, /remix, /pack, /theme-swap, /brand, /pair \u2014 wrap the designlang CLI to pull DTCG tokens, Tailwind/shadcn/Figma vars, motion + voice, generate shareable graded report cards, head-to-head battle pages, six-vocabulary remixes, downloadable design-system bundles, OKLCH-correct theme recolouring, full editorial brand-guidelines books, and design crossovers between two sites.",
|
|
4
|
-
"version": "12.10.
|
|
4
|
+
"version": "12.10.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Manavarya Singh",
|
|
7
7
|
"url": "https://github.com/Manavarya09"
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [12.11.0] — 2026-05-15
|
|
4
|
+
|
|
5
|
+
**`brand --pdf` ships native PDF brand guides.**
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx designlang brand stripe.com --pdf
|
|
9
|
+
# → stripe-com.brand.pdf (print-ready, ~3–5s)
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
What you get on top of any "save as PDF":
|
|
13
|
+
|
|
14
|
+
- **Per-chapter page breaks** — every section starts on a fresh page.
|
|
15
|
+
- **Running footer** — `designlang · <subject> · <page> of <total>` on every page.
|
|
16
|
+
- **Selectable text + embedded fonts** — never rasterized.
|
|
17
|
+
- **`--paper a4|letter|tabloid` + `--landscape`** for any output target.
|
|
18
|
+
- **`--attach-tokens`** embeds the DTCG tokens JSON *inside* the PDF as a
|
|
19
|
+
proper file attachment — open the PDF in Preview/Acrobat, hit the
|
|
20
|
+
paperclip, drop the JSON straight into Tailwind.
|
|
21
|
+
- **`--no-print-background`** strips the brand-colour cover band for a
|
|
22
|
+
smaller file when you need it.
|
|
23
|
+
|
|
24
|
+
Adds one tiny dep (`pdf-lib`, MIT, ~600KB) used only when `--attach-tokens`
|
|
25
|
+
is passed; lazy-imported behind a dynamic `import()`.
|
|
26
|
+
|
|
27
|
+
## [12.10.1] — 2026-05-13
|
|
28
|
+
|
|
29
|
+
**Tiny ship: \`stats\` now scans multiple URLs at once.**
|
|
30
|
+
|
|
31
|
+
\`designlang stats\` previously took exactly one URL. Now it takes any
|
|
32
|
+
number, runs them in parallel, and prints each block with a divider:
|
|
33
|
+
|
|
34
|
+
\`\`\`bash
|
|
35
|
+
npx designlang stats stripe.com vercel.com linear.app
|
|
36
|
+
\`\`\`
|
|
37
|
+
|
|
38
|
+
In \`--as-json\` mode:
|
|
39
|
+
|
|
40
|
+
- One URL → a bare object (legacy shape, unchanged)
|
|
41
|
+
- Two-or-more URLs → an array of objects
|
|
42
|
+
- Per-URL failures don't kill the batch — they surface as
|
|
43
|
+
\`{ url, error }\` in JSON or a red row in pretty mode, and the
|
|
44
|
+
process exits non-zero only when at least one site failed.
|
|
45
|
+
|
|
46
|
+
No flag changes, no schema breaks for the existing single-URL callers.
|
|
47
|
+
|
|
3
48
|
## [12.10.0] — 2026-05-12
|
|
4
49
|
|
|
5
50
|
**Small ship: \`designlang stats\` + low-confidence warning in grade cards.**
|
package/bin/design-extract.js
CHANGED
|
@@ -53,6 +53,7 @@ import { buildPack } from '../src/pack.js';
|
|
|
53
53
|
import { recolorDesign } from '../src/recolor.js';
|
|
54
54
|
import { formatThemeSwap, formatThemeSwapMarkdown } from '../src/formatters/theme-swap.js';
|
|
55
55
|
import { formatBrandBook, formatBrandBookMarkdown } from '../src/formatters/brand-book.js';
|
|
56
|
+
import { htmlToPdf } from '../src/pdf.js';
|
|
56
57
|
import { fuseDesigns, AXES } from '../src/fuse.js';
|
|
57
58
|
import { formatPair, formatPairMarkdown } from '../src/formatters/pair.js';
|
|
58
59
|
import { nameFromUrl } from '../src/utils.js';
|
|
@@ -946,23 +947,31 @@ program
|
|
|
946
947
|
|
|
947
948
|
// ── Stats command — fast stdout summary, no files written ──
|
|
948
949
|
program
|
|
949
|
-
.command('stats <
|
|
950
|
-
.description('Print a concise one-screen summary to stdout — grade, primary, fonts, spacing, voice. No files written.')
|
|
951
|
-
.option('-j, --as-json', 'emit machine-readable JSON to stdout instead of pretty text')
|
|
952
|
-
.action(async (
|
|
953
|
-
|
|
954
|
-
|
|
950
|
+
.command('stats <urls...>')
|
|
951
|
+
.description('Print a concise one-screen summary to stdout — grade, primary, fonts, spacing, voice. Accepts multiple URLs. No files written.')
|
|
952
|
+
.option('-j, --as-json', 'emit machine-readable JSON to stdout instead of pretty text (an array when multiple URLs)')
|
|
953
|
+
.action(async (urls, opts) => {
|
|
954
|
+
// Normalise each URL the same way the single-URL path used to.
|
|
955
|
+
const targets = urls.map(u => {
|
|
956
|
+
const full = u.startsWith('http') ? u : `https://${u}`;
|
|
957
|
+
validateUrl(full);
|
|
958
|
+
return full;
|
|
959
|
+
});
|
|
955
960
|
|
|
956
961
|
// Quiet path for --as-json: no spinner / chrome noise, just data on stdout.
|
|
957
962
|
// (`--json` is already a global program flag; `--as-json` avoids the clash.)
|
|
958
963
|
const wantJson = !!opts.asJson;
|
|
959
|
-
const spinner = wantJson
|
|
960
|
-
|
|
964
|
+
const spinner = wantJson
|
|
965
|
+
? null
|
|
966
|
+
: ora(targets.length === 1 ? `Reading ${targets[0]}...` : `Reading ${targets.length} sites in parallel...`).start();
|
|
967
|
+
|
|
968
|
+
// Single helper used by both pretty and JSON paths.
|
|
969
|
+
async function summarise(url) {
|
|
961
970
|
const design = await extractDesignLanguage(url);
|
|
962
971
|
const s = design.score || {};
|
|
963
972
|
const primary = design.colors?.primary;
|
|
964
973
|
const families = (design.typography?.families || []).map(f => f?.name || f).filter(Boolean);
|
|
965
|
-
|
|
974
|
+
return {
|
|
966
975
|
url,
|
|
967
976
|
title: design.meta?.title,
|
|
968
977
|
grade: s.grade ?? null,
|
|
@@ -985,30 +994,24 @@ program
|
|
|
985
994
|
stack: design.stack?.framework,
|
|
986
995
|
intent: design.pageIntent?.type,
|
|
987
996
|
};
|
|
997
|
+
}
|
|
988
998
|
|
|
989
|
-
|
|
990
|
-
if (spinner) spinner.stop();
|
|
991
|
-
process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
|
|
992
|
-
return;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
spinner.stop();
|
|
999
|
+
function printPretty(summary) {
|
|
996
1000
|
const gradeColor =
|
|
997
1001
|
summary.grade === 'A' ? chalk.green
|
|
998
1002
|
: summary.grade === 'B' ? chalk.cyan
|
|
999
1003
|
: summary.grade === 'C' ? chalk.yellow
|
|
1000
1004
|
: summary.grade === 'D' ? chalk.magenta
|
|
1001
1005
|
: chalk.red;
|
|
1006
|
+
const primary = summary.primary;
|
|
1002
1007
|
const confTag = primary && primary.confidence != null
|
|
1003
1008
|
? (primary.confidence < 0.5
|
|
1004
1009
|
? chalk.yellow(`~${Math.round(primary.confidence * 100)}% conf`)
|
|
1005
1010
|
: chalk.gray(`${Math.round(primary.confidence * 100)}% conf`))
|
|
1006
1011
|
: '';
|
|
1007
|
-
const line = (label, value) =>
|
|
1008
|
-
` ${chalk.gray(label.padEnd(12))} ${value}`;
|
|
1009
|
-
|
|
1012
|
+
const line = (label, value) => ` ${chalk.gray(label.padEnd(12))} ${value}`;
|
|
1010
1013
|
console.log('');
|
|
1011
|
-
console.log(` ${chalk.bold(url)}`);
|
|
1014
|
+
console.log(` ${chalk.bold(summary.url)}`);
|
|
1012
1015
|
if (summary.title) console.log(` ${chalk.gray(summary.title)}`);
|
|
1013
1016
|
console.log('');
|
|
1014
1017
|
console.log(line('Grade', `${gradeColor.bold(summary.grade || '—')} ${chalk.gray('·')} ${chalk.bold(String(summary.score ?? '—') + '/100')}`));
|
|
@@ -1017,10 +1020,10 @@ program
|
|
|
1017
1020
|
} else {
|
|
1018
1021
|
console.log(line('Primary', chalk.gray('—')));
|
|
1019
1022
|
}
|
|
1020
|
-
if (families.length) {
|
|
1021
|
-
const head = families[0];
|
|
1022
|
-
const body = families[1] || head;
|
|
1023
|
-
const extra =
|
|
1023
|
+
if (summary.families.length) {
|
|
1024
|
+
const head = summary.families[0];
|
|
1025
|
+
const body = summary.families[1] || head;
|
|
1026
|
+
const extra = summary.fontFamilyCount > 2 ? chalk.gray(` +${summary.fontFamilyCount - 2}`) : '';
|
|
1024
1027
|
console.log(line('Fonts', `${head}${body && body !== head ? chalk.gray(' / ') + body : ''}${extra}`));
|
|
1025
1028
|
} else {
|
|
1026
1029
|
console.log(line('Fonts', chalk.gray('—')));
|
|
@@ -1034,7 +1037,41 @@ program
|
|
|
1034
1037
|
console.log(line('Material', summary.material || chalk.gray('—')));
|
|
1035
1038
|
console.log(line('Tone', summary.tone || chalk.gray('—')));
|
|
1036
1039
|
console.log(line('Intent', summary.intent || chalk.gray('—')));
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
try {
|
|
1043
|
+
// Settle so a single failure doesn't kill the whole batch; per-URL
|
|
1044
|
+
// errors are surfaced as individual rejections in both paths.
|
|
1045
|
+
const results = await Promise.allSettled(targets.map(summarise));
|
|
1046
|
+
|
|
1047
|
+
if (wantJson) {
|
|
1048
|
+
if (spinner) spinner.stop();
|
|
1049
|
+
const payload = results.map((r, i) => r.status === 'fulfilled'
|
|
1050
|
+
? r.value
|
|
1051
|
+
: { url: targets[i], error: r.reason?.message || String(r.reason) });
|
|
1052
|
+
// When the caller passed a single URL we keep the legacy shape
|
|
1053
|
+
// (a bare object) so existing scripts don't break. Multiple URLs
|
|
1054
|
+
// become an array.
|
|
1055
|
+
const out = targets.length === 1 ? payload[0] : payload;
|
|
1056
|
+
process.stdout.write(JSON.stringify(out, null, 2) + '\n');
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
spinner.stop();
|
|
1061
|
+
let anyFailed = false;
|
|
1062
|
+
results.forEach((r, i) => {
|
|
1063
|
+
if (i > 0) console.log(chalk.gray(' ' + '─'.repeat(60)));
|
|
1064
|
+
if (r.status === 'fulfilled') {
|
|
1065
|
+
printPretty(r.value);
|
|
1066
|
+
} else {
|
|
1067
|
+
anyFailed = true;
|
|
1068
|
+
console.log('');
|
|
1069
|
+
console.log(` ${chalk.bold(targets[i])}`);
|
|
1070
|
+
console.log(` ${chalk.red('failed:')} ${chalk.red(r.reason?.message || String(r.reason))}`);
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1037
1073
|
console.log('');
|
|
1074
|
+
if (anyFailed) process.exitCode = 1;
|
|
1038
1075
|
} catch (err) {
|
|
1039
1076
|
if (spinner) spinner.fail('Stats failed');
|
|
1040
1077
|
console.error(chalk.red(`\n ${err.message}\n`));
|
|
@@ -1420,6 +1457,11 @@ program
|
|
|
1420
1457
|
.option('-n, --name <name>', 'output file prefix (default: derived from URL)')
|
|
1421
1458
|
.option('--format <fmt>', 'output format: html, md, json, all', 'all')
|
|
1422
1459
|
.option('--open', 'open the HTML book in the default browser')
|
|
1460
|
+
.option('--pdf', 'also emit a print-ready PDF brand guide (chapter bookmarks, running page numbers)')
|
|
1461
|
+
.option('--paper <size>', 'PDF paper size: a4 | letter | tabloid', 'a4')
|
|
1462
|
+
.option('--landscape', 'PDF landscape orientation')
|
|
1463
|
+
.option('--no-print-background', 'strip the brand-colour cover band from the PDF (smaller file)')
|
|
1464
|
+
.option('--attach-tokens', 'embed the DTCG tokens JSON as a PDF file attachment')
|
|
1423
1465
|
.action(async (url, opts) => {
|
|
1424
1466
|
if (!url.startsWith('http')) url = `https://${url}`;
|
|
1425
1467
|
validateUrl(url);
|
|
@@ -1441,11 +1483,14 @@ program
|
|
|
1441
1483
|
const prefix = opts.name || `${nameFromUrl(url)}.brand`;
|
|
1442
1484
|
const written = [];
|
|
1443
1485
|
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1486
|
+
let bookHtml = null;
|
|
1487
|
+
if (opts.format === 'all' || opts.format === 'html' || opts.pdf) {
|
|
1488
|
+
bookHtml = formatBrandBook(design, { version: PKG_VERSION });
|
|
1489
|
+
if (opts.format === 'all' || opts.format === 'html') {
|
|
1490
|
+
const p = join(outDir, `${prefix}.html`);
|
|
1491
|
+
writeFileSync(p, bookHtml);
|
|
1492
|
+
written.push(p);
|
|
1493
|
+
}
|
|
1449
1494
|
}
|
|
1450
1495
|
if (opts.format === 'all' || opts.format === 'md') {
|
|
1451
1496
|
const md = formatBrandBookMarkdown(design);
|
|
@@ -1479,6 +1524,42 @@ program
|
|
|
1479
1524
|
written.push(p);
|
|
1480
1525
|
}
|
|
1481
1526
|
|
|
1527
|
+
if (opts.pdf) {
|
|
1528
|
+
spinner.text = 'Rendering PDF...';
|
|
1529
|
+
const pdfPath = join(outDir, `${prefix}.pdf`);
|
|
1530
|
+
const attachments = [];
|
|
1531
|
+
if (opts.attachTokens) {
|
|
1532
|
+
// Reuse the JSON we just wrote if available; otherwise build a DTCG payload on the fly.
|
|
1533
|
+
let tokensJson;
|
|
1534
|
+
try {
|
|
1535
|
+
tokensJson = readFileSync(join(outDir, `${prefix}.json`));
|
|
1536
|
+
} catch {
|
|
1537
|
+
tokensJson = Buffer.from(JSON.stringify({
|
|
1538
|
+
colors: design.colors, typography: design.typography,
|
|
1539
|
+
spacing: design.spacing, motion: design.motion,
|
|
1540
|
+
}, null, 2));
|
|
1541
|
+
}
|
|
1542
|
+
attachments.push({
|
|
1543
|
+
filename: `${nameFromUrl(url)}-tokens.json`,
|
|
1544
|
+
contents: tokensJson,
|
|
1545
|
+
mimeType: 'application/json',
|
|
1546
|
+
description: 'Design tokens (DTCG-aligned)',
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
await htmlToPdf(bookHtml, {
|
|
1550
|
+
paper: opts.paper,
|
|
1551
|
+
landscape: !!opts.landscape,
|
|
1552
|
+
printBackground: opts.printBackground !== false,
|
|
1553
|
+
attachments,
|
|
1554
|
+
metadata: {
|
|
1555
|
+
title: `${new URL(url).hostname} brand guidelines`,
|
|
1556
|
+
subject: `${new URL(url).hostname} brand guidelines`,
|
|
1557
|
+
},
|
|
1558
|
+
outPath: pdfPath,
|
|
1559
|
+
});
|
|
1560
|
+
written.push(pdfPath);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1482
1563
|
spinner.stop();
|
|
1483
1564
|
const colorCount = (design.colors?.all || []).length;
|
|
1484
1565
|
const fontCount = (design.typography?.families || []).length;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "designlang",
|
|
3
|
-
"version": "12.
|
|
4
|
-
"description": "Extract the complete design language from any website and ship it
|
|
3
|
+
"version": "12.11.0",
|
|
4
|
+
"description": "Extract the complete design language from any website and ship it — clone to a working Next.js starter, guard tokens with a CI drift bot, or browse everything in a local studio. Outputs W3C DTCG tokens, motion tokens, typed anatomy stubs, Tailwind config, and ready-to-paste v0 / Lovable / Cursor / Claude-Artifacts prompts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"designlang": "./bin/design-extract.js"
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"chalk": "^5.3.0",
|
|
18
18
|
"commander": "^12.0.0",
|
|
19
19
|
"ora": "^8.0.0",
|
|
20
|
+
"pdf-lib": "^1.17.1",
|
|
20
21
|
"playwright": "^1.42.0"
|
|
21
22
|
},
|
|
22
23
|
"engines": {
|
|
@@ -48,4 +49,4 @@
|
|
|
48
49
|
],
|
|
49
50
|
"author": "masyv",
|
|
50
51
|
"license": "MIT"
|
|
51
|
-
}
|
|
52
|
+
}
|
|
@@ -945,7 +945,8 @@ export function formatBrandBook(design, opts = {}) {
|
|
|
945
945
|
.cover, .toc, section { page-break-inside: avoid; border-color: #ddd; padding: 36px 32px; }
|
|
946
946
|
.cover-band { background-color: var(--accent) !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
947
947
|
.toc, .cover { page-break-after: always; }
|
|
948
|
-
section { page-break-after: always; }
|
|
948
|
+
section { page-break-before: always; page-break-after: always; break-before: page; }
|
|
949
|
+
.topbar { display: none !important; }
|
|
949
950
|
.scale-table, .a11y-pair, .component-card { page-break-inside: avoid; }
|
|
950
951
|
}
|
|
951
952
|
</style>
|
package/src/pdf.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
import { writeFileSync } from 'fs';
|
|
3
|
+
|
|
4
|
+
export async function htmlToPdf(html, opts = {}) {
|
|
5
|
+
const {
|
|
6
|
+
paper = 'a4',
|
|
7
|
+
landscape = false,
|
|
8
|
+
printBackground = true,
|
|
9
|
+
attachments = [],
|
|
10
|
+
metadata = {},
|
|
11
|
+
outPath,
|
|
12
|
+
} = opts;
|
|
13
|
+
|
|
14
|
+
const format = String(paper).toLowerCase();
|
|
15
|
+
const browser = await chromium.launch();
|
|
16
|
+
try {
|
|
17
|
+
const page = await browser.newPage();
|
|
18
|
+
await page.setContent(html, { waitUntil: 'networkidle' });
|
|
19
|
+
|
|
20
|
+
const subject = metadata.subject || '';
|
|
21
|
+
const buffer = await page.pdf({
|
|
22
|
+
format,
|
|
23
|
+
landscape: !!landscape,
|
|
24
|
+
printBackground,
|
|
25
|
+
margin: { top: '24mm', right: '18mm', bottom: '20mm', left: '18mm' },
|
|
26
|
+
displayHeaderFooter: true,
|
|
27
|
+
headerTemplate: `<div></div>`,
|
|
28
|
+
footerTemplate: `<div style="font-family: -apple-system, sans-serif; font-size: 9px; color: #888; width: 100%; padding: 0 18mm; display: flex; justify-content: space-between;"><span>designlang${subject ? ' · ' + escapeHtml(subject) : ''}</span><span><span class="pageNumber"></span> of <span class="totalPages"></span></span></div>`,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (attachments.length) {
|
|
32
|
+
const { PDFDocument } = await import('pdf-lib');
|
|
33
|
+
const pdfDoc = await PDFDocument.load(buffer);
|
|
34
|
+
if (metadata.title) pdfDoc.setTitle(metadata.title);
|
|
35
|
+
if (metadata.subject) pdfDoc.setSubject(metadata.subject);
|
|
36
|
+
pdfDoc.setAuthor('designlang');
|
|
37
|
+
pdfDoc.setCreator('designlang');
|
|
38
|
+
for (const a of attachments) {
|
|
39
|
+
await pdfDoc.attach(a.contents, a.filename, {
|
|
40
|
+
mimeType: a.mimeType || 'application/json',
|
|
41
|
+
description: a.description || a.filename,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
writeFileSync(outPath, await pdfDoc.save());
|
|
45
|
+
} else {
|
|
46
|
+
writeFileSync(outPath, buffer);
|
|
47
|
+
}
|
|
48
|
+
} finally {
|
|
49
|
+
await browser.close();
|
|
50
|
+
}
|
|
51
|
+
return outPath;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function escapeHtml(s) {
|
|
55
|
+
return String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
56
|
+
}
|