@jjlmoya/utils-games-development 1.53.0 → 1.55.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/src/category/index.ts +2 -1
- package/src/entries.ts +6 -1
- package/src/index.ts +2 -0
- package/src/tests/contrast_legibility.test.ts +245 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/itchioGameTester/itchio-game-tester.css +3 -3
- package/src/tool/localizationSanitizer/localization-sanitizer.css +13 -13
- package/src/tool/retroSfxGenerator/bibliography.astro +6 -0
- package/src/tool/retroSfxGenerator/bibliography.ts +16 -0
- package/src/tool/retroSfxGenerator/client.ts +256 -0
- package/src/tool/retroSfxGenerator/component.astro +146 -0
- package/src/tool/retroSfxGenerator/entry.ts +27 -0
- package/src/tool/retroSfxGenerator/i18n/de.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/en.ts +224 -0
- package/src/tool/retroSfxGenerator/i18n/es.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/fr.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/id.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/it.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/ja.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/ko.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/nl.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/pl.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/pt.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/ru.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/sv.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/tr.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/zh.ts +211 -0
- package/src/tool/retroSfxGenerator/index.ts +11 -0
- package/src/tool/retroSfxGenerator/logic.test.ts +40 -0
- package/src/tool/retroSfxGenerator/logic.ts +214 -0
- package/src/tool/retroSfxGenerator/retro-sfx-generator.css +429 -0
- package/src/tool/retroSfxGenerator/seo.astro +15 -0
- package/src/tool/retroSfxGenerator/ui.ts +41 -0
- package/src/tool/saveFileEditor/game-save-file-editor.css +5 -5
- package/src/tool/spriteSheetPacker/sprite-sheet-packer.css +5 -5
- package/src/tool/steamBbcodeTranslator/app-client.ts +125 -0
- package/src/tool/steamBbcodeTranslator/bbcode-parser.ts +110 -0
- package/src/tool/steamBbcodeTranslator/bibliography.astro +6 -0
- package/src/tool/steamBbcodeTranslator/bibliography.ts +14 -0
- package/src/tool/steamBbcodeTranslator/component.astro +83 -0
- package/src/tool/steamBbcodeTranslator/entry.ts +28 -0
- package/src/tool/steamBbcodeTranslator/html-parser.ts +42 -0
- package/src/tool/steamBbcodeTranslator/html-renderer.ts +71 -0
- package/src/tool/steamBbcodeTranslator/i18n/de.ts +207 -0
- package/src/tool/steamBbcodeTranslator/i18n/en.ts +236 -0
- package/src/tool/steamBbcodeTranslator/i18n/es.ts +207 -0
- package/src/tool/steamBbcodeTranslator/i18n/fr.ts +195 -0
- package/src/tool/steamBbcodeTranslator/i18n/id.ts +195 -0
- package/src/tool/steamBbcodeTranslator/i18n/it.ts +195 -0
- package/src/tool/steamBbcodeTranslator/i18n/ja.ts +195 -0
- package/src/tool/steamBbcodeTranslator/i18n/ko.ts +195 -0
- package/src/tool/steamBbcodeTranslator/i18n/nl.ts +195 -0
- package/src/tool/steamBbcodeTranslator/i18n/pl.ts +195 -0
- package/src/tool/steamBbcodeTranslator/i18n/pt.ts +195 -0
- package/src/tool/steamBbcodeTranslator/i18n/ru.ts +195 -0
- package/src/tool/steamBbcodeTranslator/i18n/sv.ts +195 -0
- package/src/tool/steamBbcodeTranslator/i18n/tr.ts +195 -0
- package/src/tool/steamBbcodeTranslator/i18n/zh.ts +195 -0
- package/src/tool/steamBbcodeTranslator/index.ts +12 -0
- package/src/tool/steamBbcodeTranslator/logic.test.ts +41 -0
- package/src/tool/steamBbcodeTranslator/logic.ts +96 -0
- package/src/tool/steamBbcodeTranslator/markdown-parser.ts +115 -0
- package/src/tool/steamBbcodeTranslator/markdown-renderer.ts +85 -0
- package/src/tool/steamBbcodeTranslator/seo.astro +15 -0
- package/src/tool/steamBbcodeTranslator/steam-bbcode-translator.css +392 -0
- package/src/tool/steamBbcodeTranslator/storage.ts +30 -0
- package/src/tool/steamBbcodeTranslator/types.ts +36 -0
- package/src/tool/steamBbcodeTranslator/ui.ts +18 -0
- package/src/tool/steamCapsuleGenerator/steam-capsule-generator.css +6 -6
- package/src/tools.ts +4 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Node, TagNode, TextNode } from './types';
|
|
2
|
+
|
|
3
|
+
const BB_TAG_PATTERN = /\[(\/)?([a-z][a-z0-9]*|\*)(?:=([^\]]*))?\]/gi;
|
|
4
|
+
const SUPPORTED_TAGS = new Set(['h1', 'h2', 'h3', 'b', 'strong', 'i', 'em', 'u', 's', 'strike', 'url', 'code', 'quote', 'spoiler', 'list', 'olist', '*']);
|
|
5
|
+
|
|
6
|
+
export function text(value: string): TextNode {
|
|
7
|
+
return { kind: 'text', value };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function tag(name: string, value: string | undefined, children: Node[] = []): TagNode {
|
|
11
|
+
return { kind: 'tag', name, value, children };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ParseStackEntry {
|
|
15
|
+
node: TagNode;
|
|
16
|
+
parent: Node[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ParseContext {
|
|
20
|
+
stack: ParseStackEntry[];
|
|
21
|
+
root: Node[];
|
|
22
|
+
warnings: string[];
|
|
23
|
+
append: (n: Node) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function handleAsteriskTag(ctx: ParseContext, full: string): void {
|
|
27
|
+
const inList = ctx.stack.some((entry) => entry.node.name === 'list' || entry.node.name === 'olist');
|
|
28
|
+
if (!inList) {
|
|
29
|
+
ctx.warnings.push('A list item marker was found outside a list.');
|
|
30
|
+
ctx.append(text(full));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
while (ctx.stack.length) {
|
|
34
|
+
const top = ctx.stack[ctx.stack.length - 1];
|
|
35
|
+
if (top && top.node.name === '*') ctx.stack.pop();
|
|
36
|
+
else break;
|
|
37
|
+
}
|
|
38
|
+
const listParent = ctx.stack[ctx.stack.length - 1];
|
|
39
|
+
if (listParent && (listParent.node.name === 'list' || listParent.node.name === 'olist')) {
|
|
40
|
+
const item = tag('*', undefined);
|
|
41
|
+
listParent.node.children.push(item);
|
|
42
|
+
ctx.stack.push({ node: item, parent: listParent.node.children });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function handleClosingTag(ctx: ParseContext, name: string, full: string): void {
|
|
47
|
+
const matchingIndex = [...ctx.stack].reverse().findIndex((entry) => entry.node.name === name);
|
|
48
|
+
if (matchingIndex === -1) {
|
|
49
|
+
ctx.warnings.push(`Closing tag [/${name}] has no matching opening tag.`);
|
|
50
|
+
ctx.append(text(full));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
for (let i = 0; i <= matchingIndex; i += 1) ctx.stack.pop();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function handleOpeningTag(ctx: ParseContext, name: string, value?: string): void {
|
|
57
|
+
const node = tag(name, value);
|
|
58
|
+
ctx.append(node);
|
|
59
|
+
if (name !== 'br') {
|
|
60
|
+
const top = ctx.stack[ctx.stack.length - 1];
|
|
61
|
+
ctx.stack.push({ node, parent: top ? top.node.children : ctx.root });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function processMatch(ctx: ParseContext, match: RegExpExecArray, full: string, rawName: string): void {
|
|
66
|
+
const closing = Boolean(match[1]);
|
|
67
|
+
const name = rawName.toLowerCase();
|
|
68
|
+
const value = match[3]?.trim();
|
|
69
|
+
|
|
70
|
+
if (!SUPPORTED_TAGS.has(name)) {
|
|
71
|
+
ctx.warnings.push(`Unsupported tag [${name}] was kept as text.`);
|
|
72
|
+
ctx.append(text(full));
|
|
73
|
+
} else if (name === '*') {
|
|
74
|
+
handleAsteriskTag(ctx, full);
|
|
75
|
+
} else if (closing) {
|
|
76
|
+
handleClosingTag(ctx, name, full);
|
|
77
|
+
} else {
|
|
78
|
+
handleOpeningTag(ctx, name, value);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function parseBbcode(source: string): { nodes: Node[]; warnings: string[]; tagCount: number } {
|
|
83
|
+
const root: Node[] = [];
|
|
84
|
+
const stack: ParseStackEntry[] = [];
|
|
85
|
+
let cursor = 0;
|
|
86
|
+
let tagCount = 0;
|
|
87
|
+
const warnings: string[] = [];
|
|
88
|
+
|
|
89
|
+
const append = (nodeToAppend: Node) => {
|
|
90
|
+
const top = stack[stack.length - 1];
|
|
91
|
+
const target = top ? top.node.children : root;
|
|
92
|
+
target.push(nodeToAppend);
|
|
93
|
+
};
|
|
94
|
+
const ctx: ParseContext = { stack, root, warnings, append };
|
|
95
|
+
|
|
96
|
+
for (const match of source.matchAll(BB_TAG_PATTERN)) {
|
|
97
|
+
const full = match[0];
|
|
98
|
+
const index = match.index ?? 0;
|
|
99
|
+
if (index > cursor) append(text(source.slice(cursor, index)));
|
|
100
|
+
cursor = index + full.length;
|
|
101
|
+
if (!match[2]) continue;
|
|
102
|
+
tagCount += 1;
|
|
103
|
+
processMatch(ctx, match, full, match[2]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (cursor < source.length) append(text(source.slice(cursor)));
|
|
107
|
+
const top = stack[stack.length - 1];
|
|
108
|
+
if (top) warnings.push(`Unclosed tag [${top.node.name}] was closed automatically.`);
|
|
109
|
+
return { nodes: root, warnings: [...new Set(warnings)], tagCount };
|
|
110
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const bibliographyEntries: { name: string; url: string }[] = [
|
|
2
|
+
{
|
|
3
|
+
name: 'Steamworks Store Page Written Description',
|
|
4
|
+
url: 'https://partner.steamgames.com/doc/store/page/description?language=english'
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
name: 'Steamworks Importing HTML',
|
|
8
|
+
url: 'https://partner.steamgames.com/doc/marketing/event_tools/import?language=english'
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: 'itch.io Creator Documentation',
|
|
12
|
+
url: 'https://itch.io/docs/creators/getting-started'
|
|
13
|
+
}
|
|
14
|
+
];
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { SteamBbcodeTranslatorUI } from './ui';
|
|
3
|
+
import { sampleBbcode } from './logic';
|
|
4
|
+
import './steam-bbcode-translator.css';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
ui: SteamBbcodeTranslatorUI;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { ui: t } = Astro.props;
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<div
|
|
14
|
+
class="bbcode-root"
|
|
15
|
+
id="steam-bbcode-translator"
|
|
16
|
+
data-detected-label={t.detectedLabel}
|
|
17
|
+
data-detected-empty={t.detectedEmpty}
|
|
18
|
+
data-bbcode={t.bbcode}
|
|
19
|
+
data-markdown={t.markdown}
|
|
20
|
+
data-html={t.html}
|
|
21
|
+
data-copy={t.copy}
|
|
22
|
+
data-copied={t.copied}
|
|
23
|
+
data-preview-empty={t.previewEmpty}
|
|
24
|
+
data-characters={t.characters}
|
|
25
|
+
data-blocks={t.blocks}
|
|
26
|
+
>
|
|
27
|
+
<div class="bbcode-header">
|
|
28
|
+
<div class="bbcode-title-row">
|
|
29
|
+
<span class="bbcode-mark" aria-hidden="true">[ / ]</span>
|
|
30
|
+
<div>
|
|
31
|
+
<span class="bbcode-kicker">STEAM MARKUP TRANSLATOR</span>
|
|
32
|
+
<strong>{t.privacyNote}</strong>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
<span class="bbcode-draft-note">{t.persistenceNote}</span>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<section class="bbcode-input-card">
|
|
39
|
+
<div class="bbcode-section-head">
|
|
40
|
+
<div>
|
|
41
|
+
<span class="bbcode-step">01</span>
|
|
42
|
+
<h2>{t.editorLabel}</h2>
|
|
43
|
+
</div>
|
|
44
|
+
<span class="bbcode-detected" id="bbcode-detected">{t.detectedEmpty}</span>
|
|
45
|
+
</div>
|
|
46
|
+
<label class="bbcode-sr-only" for="bbcode-source">{t.editorLabel}</label>
|
|
47
|
+
<textarea id="bbcode-source" spellcheck="false" aria-describedby="bbcode-editor-hint">{sampleBbcode}</textarea>
|
|
48
|
+
<div class="bbcode-input-footer">
|
|
49
|
+
<p id="bbcode-editor-hint">{t.editorHint}</p>
|
|
50
|
+
<div class="bbcode-input-meta">
|
|
51
|
+
<span><strong id="bbcode-character-count">0</strong> {t.characters}</span>
|
|
52
|
+
<span><strong id="bbcode-block-count">0</strong> {t.blocks}</span>
|
|
53
|
+
<button type="button" class="bbcode-clear-button" id="bbcode-clear">{t.clear}</button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</section>
|
|
57
|
+
|
|
58
|
+
<section class="bbcode-output-section" aria-live="polite">
|
|
59
|
+
<div class="bbcode-output-heading">
|
|
60
|
+
<span class="bbcode-step">02</span>
|
|
61
|
+
<h2>{t.detectedLabel}</h2>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="bbcode-output-grid" id="bbcode-output-grid"></div>
|
|
64
|
+
</section>
|
|
65
|
+
|
|
66
|
+
<section class="bbcode-preview-card">
|
|
67
|
+
<div class="bbcode-output-heading">
|
|
68
|
+
<span class="bbcode-step">03</span>
|
|
69
|
+
<h2>{t.previewLabel}</h2>
|
|
70
|
+
</div>
|
|
71
|
+
<div id="bbcode-preview" class="bbcode-preview-surface"><p>{t.previewEmpty}</p></div>
|
|
72
|
+
</section>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<script>
|
|
76
|
+
import { initSteamBbcodeTranslatorApp } from './app-client';
|
|
77
|
+
|
|
78
|
+
if (document.readyState === 'loading') {
|
|
79
|
+
document.addEventListener('DOMContentLoaded', initSteamBbcodeTranslatorApp);
|
|
80
|
+
} else {
|
|
81
|
+
initSteamBbcodeTranslatorApp();
|
|
82
|
+
}
|
|
83
|
+
</script>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { GamesToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { SteamBbcodeTranslatorUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type { SteamBbcodeTranslatorUI };
|
|
5
|
+
|
|
6
|
+
export type SteamBbcodeTranslatorLocaleContent = ToolLocaleContent<SteamBbcodeTranslatorUI>;
|
|
7
|
+
|
|
8
|
+
export const steamBbcodeTranslator: GamesToolEntry<SteamBbcodeTranslatorUI> = {
|
|
9
|
+
id: 'steam-bbcode-translator',
|
|
10
|
+
icons: { bg: 'mdi:gamepad-variant', fg: 'mdi:code-brackets' },
|
|
11
|
+
i18n: {
|
|
12
|
+
de: () => import('./i18n/de').then((m) => m.content),
|
|
13
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
14
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
15
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
16
|
+
id: () => import('./i18n/id').then((m) => m.content),
|
|
17
|
+
it: () => import('./i18n/it').then((m) => m.content),
|
|
18
|
+
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
19
|
+
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
20
|
+
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
21
|
+
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
22
|
+
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
23
|
+
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
24
|
+
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
25
|
+
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
26
|
+
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { TranslationResult } from './types';
|
|
2
|
+
|
|
3
|
+
export function decodeHtml(value: string): string {
|
|
4
|
+
return value
|
|
5
|
+
.replace(/</g, '<')
|
|
6
|
+
.replace(/>/g, '>')
|
|
7
|
+
.replace(/"/g, '"')
|
|
8
|
+
.replace(/'/g, "'")
|
|
9
|
+
.replace(/&/g, '&');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function countBlocks(source: string): number {
|
|
13
|
+
return source.trim() ? source.split(/\n{2,}/).filter(Boolean).length : 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function htmlToBbcode(source: string): TranslationResult {
|
|
17
|
+
let output = source.replace(/<!--[\s\S]*?-->/g, '');
|
|
18
|
+
const warnings: string[] = [];
|
|
19
|
+
output = output.replace(/<h([1-3])\b[^>]*>/gi, (_, level: string) => `[h${level}]`);
|
|
20
|
+
output = output.replace(/<\/h([1-3])>/gi, (_, level: string) => `[/h${level}]`);
|
|
21
|
+
const replacements: [RegExp, string][] = [
|
|
22
|
+
[/<(?:strong|b)\b[^>]*>/gi, '[b]'], [/<\/(?:strong|b)>/gi, '[/b]'],
|
|
23
|
+
[/<(?:em|i)\b[^>]*>/gi, '[i]'], [/<\/(?:em|i)>/gi, '[/i]'],
|
|
24
|
+
[/<u\b[^>]*>/gi, '[u]'], [/<\/u>/gi, '[/u]'],
|
|
25
|
+
[/<(?:del|s|strike)\b[^>]*>/gi, '[s]'], [/<\/(?:del|s|strike)>/gi, '[/s]'],
|
|
26
|
+
[/<blockquote\b[^>]*>/gi, '[quote]'], [/<\/blockquote>/gi, '[/quote]'],
|
|
27
|
+
[/<code\b[^>]*>/gi, '[code]'], [/<\/code>/gi, '[/code]'],
|
|
28
|
+
[/<pre\b[^>]*>/gi, ''], [/<\/pre>/gi, ''],
|
|
29
|
+
[/<details\b[^>]*>/gi, '[spoiler]'], [/<\/details>/gi, '[/spoiler]'],
|
|
30
|
+
[/<summary\b[^>]*>.*?<\/summary>/gi, ''],
|
|
31
|
+
[/<ul\b[^>]*>/gi, '[list]'], [/<\/ul>/gi, '[/list]'],
|
|
32
|
+
[/<ol\b[^>]*>/gi, '[olist]'], [/<\/ol>/gi, '[/olist]'],
|
|
33
|
+
[/<li\b[^>]*>/gi, '[*]'], [/<\/li>/gi, '\n'],
|
|
34
|
+
[/<br\s*\/?>/gi, '\n'], [/<p\b[^>]*>/gi, ''], [/<\/p>/gi, '\n\n']
|
|
35
|
+
];
|
|
36
|
+
for (const [pattern, replacement] of replacements) output = output.replace(pattern, replacement);
|
|
37
|
+
output = output.replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, '[url=$1]$2[/url]');
|
|
38
|
+
if (/<[a-z][^>]*>/i.test(output)) warnings.push('Unsupported HTML tags were removed from the BBCode result.');
|
|
39
|
+
output = decodeHtml(output.replace(/<[^>]+>/g, '')).replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
40
|
+
const tagsCount = (source.match(/<\/?[a-z][^>]*>/gi) ?? []).length;
|
|
41
|
+
return { output, warnings, stats: { characters: output.length, tags: tagsCount, blocks: countBlocks(output) } };
|
|
42
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Node, TagNode } from './types';
|
|
2
|
+
|
|
3
|
+
export function escapeHtml(value: string): string {
|
|
4
|
+
return value.replace(/[&<>"']/g, (character) => {
|
|
5
|
+
if (character === '&') return '&';
|
|
6
|
+
if (character === '<') return '<';
|
|
7
|
+
if (character === '>') return '>';
|
|
8
|
+
if (character === '"') return '"';
|
|
9
|
+
return ''';
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function inlineHtml(nodes: Node[]): string {
|
|
14
|
+
return nodes.map((node) => node.kind === 'text' ? escapeHtml(node.value) : renderHtmlNode(node, true)).join('');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function renderHtmlList(node: TagNode, ordered: boolean): string {
|
|
18
|
+
const items = node.children.filter((child): child is TagNode => child.kind === 'tag' && child.name === '*');
|
|
19
|
+
const tagName = ordered ? 'ol' : 'ul';
|
|
20
|
+
const listItems = items.map((item) => {
|
|
21
|
+
const inner = item.children.map((child) => {
|
|
22
|
+
if (child.kind === 'tag' && (child.name === 'list' || child.name === 'olist')) return renderHtmlNode(child);
|
|
23
|
+
if (child.kind === 'text') return escapeHtml(child.value);
|
|
24
|
+
return renderHtmlNode(child, true);
|
|
25
|
+
}).join('');
|
|
26
|
+
return `<li>${inner}</li>`;
|
|
27
|
+
}).join('');
|
|
28
|
+
return `<${tagName}>${listItems}</${tagName}>`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renderHtmlHeading(name: string, content: string): string | null {
|
|
32
|
+
if (name === 'h1') return `<h1>${content.trim()}</h1>`;
|
|
33
|
+
if (name === 'h2') return `<h2>${content.trim()}</h2>`;
|
|
34
|
+
if (name === 'h3') return `<h3>${content.trim()}</h3>`;
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderHtmlInlineStyle(name: string, content: string): string | null {
|
|
39
|
+
if (name === 'b' || name === 'strong') return `<strong>${content}</strong>`;
|
|
40
|
+
if (name === 'i' || name === 'em') return `<em>${content}</em>`;
|
|
41
|
+
if (name === 'u') return `<u>${content}</u>`;
|
|
42
|
+
if (name === 's' || name === 'strike') return `<del>${content}</del>`;
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function renderHtmlUrlOrCode(name: string, content: string, value?: string, inline = false): string {
|
|
47
|
+
if (name === 'url') return value ? `<a href="${escapeHtml(value)}">${content || escapeHtml(value)}</a>` : content;
|
|
48
|
+
if (name === 'code') return inline ? `<code>${content}</code>` : `<pre><code>${content}</code></pre>`;
|
|
49
|
+
return content;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function renderHtmlFormatNode(name: string, content: string, value?: string, inline = false): string {
|
|
53
|
+
const heading = renderHtmlHeading(name, content);
|
|
54
|
+
if (heading !== null) return heading;
|
|
55
|
+
const inlineStyle = renderHtmlInlineStyle(name, content);
|
|
56
|
+
if (inlineStyle !== null) return inlineStyle;
|
|
57
|
+
return renderHtmlUrlOrCode(name, content, value, inline);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function renderHtmlNode(node: TagNode, inline = false): string {
|
|
61
|
+
const content = inlineHtml(node.children);
|
|
62
|
+
if (node.name === 'quote') return `<blockquote>${content}</blockquote>`;
|
|
63
|
+
if (node.name === 'spoiler') return `<details><summary>Spoiler</summary>${content}</details>`;
|
|
64
|
+
if (node.name === 'list') return renderHtmlList(node, false);
|
|
65
|
+
if (node.name === 'olist') return renderHtmlList(node, true);
|
|
66
|
+
return renderHtmlFormatNode(node.name, content, node.value, inline);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function renderHtml(nodes: Node[]): string {
|
|
70
|
+
return nodes.map((node) => node.kind === 'text' ? escapeHtml(node.value) : renderHtmlNode(node)).join('').replace(/\n{2,}/g, '<br>');
|
|
71
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { SteamBbcodeTranslatorUI } from '../ui';
|
|
3
|
+
import { bibliographyEntries } from '../bibliography';
|
|
4
|
+
|
|
5
|
+
export const content: ToolLocaleContent<SteamBbcodeTranslatorUI> = {
|
|
6
|
+
slug: 'steam-bbcode-konverter',
|
|
7
|
+
title: 'Steam BBCode, Markdown und HTML Konverter',
|
|
8
|
+
description: 'Konvertieren Sie zwischen Steam BBCode, Markdown und HTML in beide Richtungen mit automatischer Syntaxerkennung und Vorschau.',
|
|
9
|
+
ui: {
|
|
10
|
+
editorLabel: 'Fügen Sie Ihren Text ein',
|
|
11
|
+
editorHint: 'BBCode, Markdown oder HTML wird beim Tippen automatisch erkannt.',
|
|
12
|
+
detectedLabel: 'Erkannt',
|
|
13
|
+
detectedEmpty: 'Warten auf Text',
|
|
14
|
+
bbcode: 'Steam BBCode',
|
|
15
|
+
markdown: 'Markdown',
|
|
16
|
+
html: 'HTML',
|
|
17
|
+
clear: 'Leeren',
|
|
18
|
+
copy: 'Ausgabe kopieren',
|
|
19
|
+
copied: 'In Zwischenablage kopiert',
|
|
20
|
+
characters: 'Zeichen',
|
|
21
|
+
blocks: 'Blöcke',
|
|
22
|
+
privacyNote: 'Läuft lokal im Browser. Kein Upload.',
|
|
23
|
+
persistenceNote: 'Letzter Entwurf lokal gespeichert',
|
|
24
|
+
previewLabel: 'Vorschau',
|
|
25
|
+
previewEmpty: 'Ihre formatierte Vorschau erscheint hier.'
|
|
26
|
+
},
|
|
27
|
+
seo: [
|
|
28
|
+
{
|
|
29
|
+
type: 'title',
|
|
30
|
+
level: 2,
|
|
31
|
+
text: 'Warum Shopbeschreibungen einen Konverter benötigen'
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: 'paragraph',
|
|
35
|
+
html: 'Steam Shopbeschreibungen nutzen BBCode im Steamworks Editor. Pressekits oder Dokumentationen erwarten oft Markdown oder HTML. Die manuelle Konvertierung ist zeitraubend und fehleranfällig. Dieser Konverter generiert automatisch die anderen beiden Formate.'
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: 'title',
|
|
39
|
+
level: 2,
|
|
40
|
+
text: 'Unterstützte Formate und Tags'
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
type: 'paragraph',
|
|
44
|
+
html: 'Der Konverter erkennt Überschriften, Fett- und Kursivdruck, Links, Listen, Zitate und Spoiler. Er überträgt Strukturen nahtlos zwischen allen drei Formaten.'
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: 'stats',
|
|
48
|
+
columns: 4,
|
|
49
|
+
items: [
|
|
50
|
+
{ label: 'Eingabeformate', value: '3' },
|
|
51
|
+
{ label: 'Ausgaben pro Einfügen', value: '2' },
|
|
52
|
+
{ label: 'Listen-Tiefe', value: 'Verschachtelt' },
|
|
53
|
+
{ label: 'Verarbeitung', value: 'Nur im Browser' }
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: 'title',
|
|
58
|
+
level: 2,
|
|
59
|
+
text: 'Verschachtelte Listen bleiben erhalten'
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
type: 'paragraph',
|
|
63
|
+
html: 'Statt blinder Ersetzung baut das Tool einen leichten Strukturbaum auf. So bleiben untergeordnete Listenpunkte genau an ihrer Position.'
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
type: 'table',
|
|
67
|
+
headers: ['Steam BBCode', 'Markdown', 'HTML'],
|
|
68
|
+
rows: [
|
|
69
|
+
['[h1]Titel[/h1]', '# Titel', '<h1>Titel</h1>'],
|
|
70
|
+
['[b]Wichtig[/b]', '**Wichtig**', '<strong>Wichtig</strong>'],
|
|
71
|
+
['[i]Hinweis[/i]', '*Hinweis*', '<em>Hinweis</em>'],
|
|
72
|
+
['[url=https://example.com]Link[/url]', '[Link](https://example.com)', '<a href="https://example.com">Link</a>'],
|
|
73
|
+
['[list][*]Eins[*]Zwei[/list]', '- Eins\n- Zwei', '<ul><li>Eins</li><li>Zwei</li></ul>']
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
type: 'title',
|
|
78
|
+
level: 2,
|
|
79
|
+
text: 'Unterschiede zwischen Markdown und HTML'
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: 'paragraph',
|
|
83
|
+
html: 'Markdown ist gut lesbar, unterstützt aber Unterstreichungen nicht nativ. In solchen Fällen nutzt der Konverter Inline HTML.'
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: 'tip',
|
|
87
|
+
title: 'Praktische Überprüfung vor der Veröffentlichung',
|
|
88
|
+
html: 'Vergleichen Sie vor dem Veröffentlichen das Ergebnis in der Vorschau mit dem Originaldokument.'
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
type: 'title',
|
|
92
|
+
level: 2,
|
|
93
|
+
text: 'Datenschutz für Ihren Shoptext'
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
type: 'paragraph',
|
|
97
|
+
html: 'Die Konvertierung erfolgt vollständig lokal im Browser. Es werden keine Entwürfe auf externe Server hochgeladen.'
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
type: 'title',
|
|
101
|
+
level: 2,
|
|
102
|
+
text: 'Einschränkungen'
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
type: 'proscons',
|
|
106
|
+
title: 'Vor und Nachteile der Konvertierung',
|
|
107
|
+
items: [
|
|
108
|
+
{
|
|
109
|
+
pro: 'Verschachtelte Listen werden strukturell verarbeitet.',
|
|
110
|
+
con: 'Spezielle benutzerdefinierte Tags erfordern manuelle Nachbearbeitung.'
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
pro: 'Sofortige Live Aktualisierung beim Tippen.',
|
|
114
|
+
con: 'Steam-spezifische Widgets werden nicht konvertiert.'
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
type: 'title',
|
|
120
|
+
level: 2,
|
|
121
|
+
text: 'Begriffsglossar'
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
type: 'glossary',
|
|
125
|
+
items: [
|
|
126
|
+
{
|
|
127
|
+
term: 'BBCode',
|
|
128
|
+
definition: 'Eine Tag-basierte Auszeichnungssprache für Steam-Shopseiten.'
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
term: 'Markdown',
|
|
132
|
+
definition: 'Eine einfache Textformatierung für Dokumentation und Web.'
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
term: 'HTML',
|
|
136
|
+
definition: 'Die Standard-Auszeichnungssprache für Webseiten.'
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
],
|
|
141
|
+
faqTitle: 'Häufig gestellte Fragen zur Konvertierung',
|
|
142
|
+
faq: [
|
|
143
|
+
{
|
|
144
|
+
question: 'Werden Texte an einen Server gesendet?',
|
|
145
|
+
answer: 'Nein. Die Konvertierung erfolgt vollständig lokal in Ihrem Browser.'
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
question: 'Werden verschachtelte Listen unterstützt?',
|
|
149
|
+
answer: 'Ja. Die Struktur wird vor der Generierung von Markdown oder HTML analysiert.'
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
question: 'Kann ich auch Markdown als Eingabe nutzen?',
|
|
153
|
+
answer: 'Ja. Das Format wird automatisch erkannt.'
|
|
154
|
+
}
|
|
155
|
+
],
|
|
156
|
+
howTo: [
|
|
157
|
+
{
|
|
158
|
+
name: 'Text einfügen',
|
|
159
|
+
text: 'Fügen Sie Steam BBCode, Markdown oder HTML in den Editor ein.'
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: 'Automatische Erkennung nutzen',
|
|
163
|
+
text: 'Das Tool generiert sofort die anderen Formate.'
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: 'Ergebnis kopieren',
|
|
167
|
+
text: 'Kopieren Sie das gewünschte Zielformat.'
|
|
168
|
+
}
|
|
169
|
+
],
|
|
170
|
+
schemas: [
|
|
171
|
+
{
|
|
172
|
+
'@context': 'https://schema.org',
|
|
173
|
+
'@type': 'SoftwareApplication',
|
|
174
|
+
name: 'Steam BBCode, Markdown und HTML Konverter',
|
|
175
|
+
applicationCategory: 'DeveloperApplication',
|
|
176
|
+
operatingSystem: 'Any',
|
|
177
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' }
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
'@context': 'https://schema.org',
|
|
181
|
+
'@type': 'FAQPage',
|
|
182
|
+
mainEntity: [
|
|
183
|
+
{
|
|
184
|
+
'@type': 'Question',
|
|
185
|
+
name: 'Werden Texte an einen Server gesendet?',
|
|
186
|
+
acceptedAnswer: {
|
|
187
|
+
'@type': 'Answer',
|
|
188
|
+
text: 'Nein. Die Konvertierung erfolgt vollständig lokal in Ihrem Browser.'
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
]
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
'@context': 'https://schema.org',
|
|
195
|
+
'@type': 'HowTo',
|
|
196
|
+
name: 'So konvertieren Sie Steam BBCode, Markdown und HTML',
|
|
197
|
+
step: [
|
|
198
|
+
{
|
|
199
|
+
'@type': 'HowToStep',
|
|
200
|
+
name: 'Text einfügen',
|
|
201
|
+
text: 'Fügen Sie Steam BBCode, Markdown oder HTML in den Editor ein.'
|
|
202
|
+
}
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
],
|
|
206
|
+
bibliography: bibliographyEntries
|
|
207
|
+
};
|