@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.
Files changed (72) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +6 -1
  4. package/src/index.ts +2 -0
  5. package/src/tests/contrast_legibility.test.ts +245 -0
  6. package/src/tests/locale_completeness.test.ts +2 -2
  7. package/src/tests/tool_validation.test.ts +2 -2
  8. package/src/tool/itchioGameTester/itchio-game-tester.css +3 -3
  9. package/src/tool/localizationSanitizer/localization-sanitizer.css +13 -13
  10. package/src/tool/retroSfxGenerator/bibliography.astro +6 -0
  11. package/src/tool/retroSfxGenerator/bibliography.ts +16 -0
  12. package/src/tool/retroSfxGenerator/client.ts +256 -0
  13. package/src/tool/retroSfxGenerator/component.astro +146 -0
  14. package/src/tool/retroSfxGenerator/entry.ts +27 -0
  15. package/src/tool/retroSfxGenerator/i18n/de.ts +211 -0
  16. package/src/tool/retroSfxGenerator/i18n/en.ts +224 -0
  17. package/src/tool/retroSfxGenerator/i18n/es.ts +211 -0
  18. package/src/tool/retroSfxGenerator/i18n/fr.ts +211 -0
  19. package/src/tool/retroSfxGenerator/i18n/id.ts +211 -0
  20. package/src/tool/retroSfxGenerator/i18n/it.ts +211 -0
  21. package/src/tool/retroSfxGenerator/i18n/ja.ts +211 -0
  22. package/src/tool/retroSfxGenerator/i18n/ko.ts +211 -0
  23. package/src/tool/retroSfxGenerator/i18n/nl.ts +211 -0
  24. package/src/tool/retroSfxGenerator/i18n/pl.ts +211 -0
  25. package/src/tool/retroSfxGenerator/i18n/pt.ts +211 -0
  26. package/src/tool/retroSfxGenerator/i18n/ru.ts +211 -0
  27. package/src/tool/retroSfxGenerator/i18n/sv.ts +211 -0
  28. package/src/tool/retroSfxGenerator/i18n/tr.ts +211 -0
  29. package/src/tool/retroSfxGenerator/i18n/zh.ts +211 -0
  30. package/src/tool/retroSfxGenerator/index.ts +11 -0
  31. package/src/tool/retroSfxGenerator/logic.test.ts +40 -0
  32. package/src/tool/retroSfxGenerator/logic.ts +214 -0
  33. package/src/tool/retroSfxGenerator/retro-sfx-generator.css +429 -0
  34. package/src/tool/retroSfxGenerator/seo.astro +15 -0
  35. package/src/tool/retroSfxGenerator/ui.ts +41 -0
  36. package/src/tool/saveFileEditor/game-save-file-editor.css +5 -5
  37. package/src/tool/spriteSheetPacker/sprite-sheet-packer.css +5 -5
  38. package/src/tool/steamBbcodeTranslator/app-client.ts +125 -0
  39. package/src/tool/steamBbcodeTranslator/bbcode-parser.ts +110 -0
  40. package/src/tool/steamBbcodeTranslator/bibliography.astro +6 -0
  41. package/src/tool/steamBbcodeTranslator/bibliography.ts +14 -0
  42. package/src/tool/steamBbcodeTranslator/component.astro +83 -0
  43. package/src/tool/steamBbcodeTranslator/entry.ts +28 -0
  44. package/src/tool/steamBbcodeTranslator/html-parser.ts +42 -0
  45. package/src/tool/steamBbcodeTranslator/html-renderer.ts +71 -0
  46. package/src/tool/steamBbcodeTranslator/i18n/de.ts +207 -0
  47. package/src/tool/steamBbcodeTranslator/i18n/en.ts +236 -0
  48. package/src/tool/steamBbcodeTranslator/i18n/es.ts +207 -0
  49. package/src/tool/steamBbcodeTranslator/i18n/fr.ts +195 -0
  50. package/src/tool/steamBbcodeTranslator/i18n/id.ts +195 -0
  51. package/src/tool/steamBbcodeTranslator/i18n/it.ts +195 -0
  52. package/src/tool/steamBbcodeTranslator/i18n/ja.ts +195 -0
  53. package/src/tool/steamBbcodeTranslator/i18n/ko.ts +195 -0
  54. package/src/tool/steamBbcodeTranslator/i18n/nl.ts +195 -0
  55. package/src/tool/steamBbcodeTranslator/i18n/pl.ts +195 -0
  56. package/src/tool/steamBbcodeTranslator/i18n/pt.ts +195 -0
  57. package/src/tool/steamBbcodeTranslator/i18n/ru.ts +195 -0
  58. package/src/tool/steamBbcodeTranslator/i18n/sv.ts +195 -0
  59. package/src/tool/steamBbcodeTranslator/i18n/tr.ts +195 -0
  60. package/src/tool/steamBbcodeTranslator/i18n/zh.ts +195 -0
  61. package/src/tool/steamBbcodeTranslator/index.ts +12 -0
  62. package/src/tool/steamBbcodeTranslator/logic.test.ts +41 -0
  63. package/src/tool/steamBbcodeTranslator/logic.ts +96 -0
  64. package/src/tool/steamBbcodeTranslator/markdown-parser.ts +115 -0
  65. package/src/tool/steamBbcodeTranslator/markdown-renderer.ts +85 -0
  66. package/src/tool/steamBbcodeTranslator/seo.astro +15 -0
  67. package/src/tool/steamBbcodeTranslator/steam-bbcode-translator.css +392 -0
  68. package/src/tool/steamBbcodeTranslator/storage.ts +30 -0
  69. package/src/tool/steamBbcodeTranslator/types.ts +36 -0
  70. package/src/tool/steamBbcodeTranslator/ui.ts +18 -0
  71. package/src/tool/steamCapsuleGenerator/steam-capsule-generator.css +6 -6
  72. package/src/tools.ts +4 -0
@@ -0,0 +1,195 @@
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-donusturucu',
7
+ title: 'Steam BBCode, Markdown ve HTML Donusturucu',
8
+ description: 'Steam BBCode, Markdown ve HTML formatlari arasinda otomatik sozvarligi tespiti ve canli onizleme ile cift yonlu donusturme yapin.',
9
+ ui: {
10
+ editorLabel: 'Bicimlendirilmis metninizi yapistirin',
11
+ editorHint: 'BBCode, Markdown veya HTML siz yazarken otomatik olarak algilanir.',
12
+ detectedLabel: 'Algilanan',
13
+ detectedEmpty: 'Metin bekleniyor',
14
+ bbcode: 'Steam BBCode',
15
+ markdown: 'Markdown',
16
+ html: 'HTML',
17
+ clear: 'Temizle',
18
+ copy: 'Sonucu kopyala',
19
+ copied: 'Panoya kopyalandi',
20
+ characters: 'Karakterler',
21
+ blocks: 'Bloklar',
22
+ privacyNote: 'Tarayicinizda calisir. Hicbir sey yuklenmez.',
23
+ persistenceNote: 'Son taslak yerel olarak kaydedildi',
24
+ previewLabel: 'Onizleme',
25
+ previewEmpty: 'Bicimlendirilmis onizlemeniz burada gorunur.'
26
+ },
27
+ seo: [
28
+ {
29
+ type: 'title',
30
+ level: 2,
31
+ text: 'Magaza aciklamalari neden donusturucuye ihtiyac duyar'
32
+ },
33
+ {
34
+ type: 'paragraph',
35
+ html: 'Steam magaza aciklamalari BBCode kullanir. Basin kitleri veya dokuman siteleri Markdown veya HTML gerektirir. Bu arac formatlar arasinda otomatik donusum saglar.'
36
+ },
37
+ {
38
+ type: 'title',
39
+ level: 2,
40
+ text: 'Desteklenen etiketler'
41
+ },
42
+ {
43
+ type: 'paragraph',
44
+ html: 'Basliklar, kalin, italik, baglantilar, listeler, alintilar ve spoiler etiketleri desteklenir.'
45
+ },
46
+ {
47
+ type: 'stats',
48
+ columns: 4,
49
+ items: [
50
+ { label: 'Girdi formati', value: '3' },
51
+ { label: 'Cikti formati', value: '2' },
52
+ { label: 'Liste derinligi', value: 'Ic ice' },
53
+ { label: 'Islem', value: 'Sadece tarayici' }
54
+ ]
55
+ },
56
+ {
57
+ type: 'title',
58
+ level: 2,
59
+ text: 'Ic ice listelerin korunmasi'
60
+ },
61
+ {
62
+ type: 'paragraph',
63
+ html: 'Hiyerarsik yapi agaci alt listelerin ana oge icinde kalmasini saglar.'
64
+ },
65
+ {
66
+ type: 'table',
67
+ headers: ['Steam BBCode', 'Markdown', 'HTML'],
68
+ rows: [
69
+ ['[h1]Baslik[/h1]', '# Baslik', '&lt;h1&gt;Baslik&lt;/h1&gt;'],
70
+ ['[b]Onemli[/b]', '**Onemli**', '&lt;strong&gt;Onemli&lt;/strong&gt;'],
71
+ ['[i]Not[/i]', '*Not*', '&lt;em&gt;Not&lt;/em&gt;'],
72
+ ['[url=https://example.com]Baglanti[/url]', '[Baglanti](https://example.com)', '&lt;a href="https://example.com"&gt;Baglanti&lt;/a&gt;'],
73
+ ['[list][*]Bir[*]Iki[/list]', '- Bir\n- Iki', '&lt;ul&gt;&lt;li&gt;Bir&lt;/li&gt;&lt;li&gt;Iki&lt;/li&gt;&lt;/ul&gt;']
74
+ ]
75
+ },
76
+ {
77
+ type: 'title',
78
+ level: 2,
79
+ text: 'Markdown ve HTML farkliliklari'
80
+ },
81
+ {
82
+ type: 'paragraph',
83
+ html: 'Markdown alti cizili metni yerel olarak desteklemediginde HTML etiketleri kullanilir.'
84
+ },
85
+ {
86
+ type: 'tip',
87
+ title: 'Yayinlama oncesi kontrol',
88
+ html: 'Yayinlamadan once onizleme ekranindaki ciktilari kaynak belgenizle karsilastirin.'
89
+ },
90
+ {
91
+ type: 'title',
92
+ level: 2,
93
+ text: 'Veri gizliligi'
94
+ },
95
+ {
96
+ type: 'paragraph',
97
+ html: 'Tum islemler tamamen yerel olarak tarayicinizda gerceklesir.'
98
+ },
99
+ {
100
+ type: 'title',
101
+ level: 2,
102
+ text: 'Sinirlamalar'
103
+ },
104
+ {
105
+ type: 'proscons',
106
+ title: 'Dikkat edilecekler',
107
+ items: [
108
+ {
109
+ pro: 'Liste yapisi korunur.',
110
+ con: 'Ozel etiketler manuel kontrol gerektirebilir.'
111
+ }
112
+ ]
113
+ },
114
+ {
115
+ type: 'title',
116
+ level: 2,
117
+ text: 'Terimler sozlugu'
118
+ },
119
+ {
120
+ type: 'glossary',
121
+ items: [
122
+ {
123
+ term: 'BBCode',
124
+ definition: 'Steam tarafindan kullanilan kosebenti etiket yapisi.'
125
+ },
126
+ {
127
+ term: 'Markdown',
128
+ definition: 'Kolay okunabilir duz metin bicimlendirme dili.'
129
+ },
130
+ {
131
+ term: 'HTML',
132
+ definition: 'Web sayfalari icin standart isaretleme dili.'
133
+ }
134
+ ]
135
+ }
136
+ ],
137
+ faqTitle: 'Sikca sorulan sorular',
138
+ faq: [
139
+ {
140
+ question: 'Metnim bir sunucuya gonderiliyor mu?',
141
+ answer: 'Hayir. Donusum tamamen tarayicinizda gerceklesir.'
142
+ },
143
+ {
144
+ question: 'Ic ice listeler destekleniyor mu?',
145
+ answer: 'Evet. Liste yapisi donusumden once cozümlenir.'
146
+ }
147
+ ],
148
+ howTo: [
149
+ {
150
+ name: 'Metni yapistirin',
151
+ text: 'Steam BBCode, Markdown veya HTML yapistirin.'
152
+ },
153
+ {
154
+ name: 'Otomatik donusum',
155
+ text: 'Diger iki format aninda olusturulur.'
156
+ }
157
+ ],
158
+ schemas: [
159
+ {
160
+ '@context': 'https://schema.org',
161
+ '@type': 'SoftwareApplication',
162
+ name: 'Steam BBCode, Markdown ve HTML Donusturucu',
163
+ applicationCategory: 'DeveloperApplication',
164
+ operatingSystem: 'Any',
165
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'TRY' }
166
+ },
167
+ {
168
+ '@context': 'https://schema.org',
169
+ '@type': 'FAQPage',
170
+ mainEntity: [
171
+ {
172
+ '@type': 'Question',
173
+ name: 'Metnim bir sunucuya gonderiliyor mu?',
174
+ acceptedAnswer: {
175
+ '@type': 'Answer',
176
+ text: 'Hayir. Donusum tamamen tarayicinizda gerceklesir.'
177
+ }
178
+ }
179
+ ]
180
+ },
181
+ {
182
+ '@context': 'https://schema.org',
183
+ '@type': 'HowTo',
184
+ name: 'Steam BBCode, Markdown ve HTML nasil donusturulur',
185
+ step: [
186
+ {
187
+ '@type': 'HowToStep',
188
+ name: 'Metni yapistirin',
189
+ text: 'Steam BBCode, Markdown veya HTML yapistirin.'
190
+ }
191
+ ]
192
+ }
193
+ ],
194
+ bibliography: bibliographyEntries
195
+ };
@@ -0,0 +1,195 @@
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-translator',
7
+ title: 'Steam BBCode, Markdown 与 HTML 转换器',
8
+ description: '支持 Steam BBCode、Markdown 和 HTML 之间的双向自动识别转换与实时预览。',
9
+ ui: {
10
+ editorLabel: '粘贴您的富文本',
11
+ editorHint: '输入时将自动识别 BBCode、Markdown 或 HTML 格式。',
12
+ detectedLabel: '识别格式',
13
+ detectedEmpty: '等待输入文本',
14
+ bbcode: 'Steam BBCode',
15
+ markdown: 'Markdown',
16
+ html: 'HTML',
17
+ clear: '清空',
18
+ copy: '复制输出',
19
+ copied: '已复制到剪贴板',
20
+ characters: '字符数',
21
+ blocks: '区块数',
22
+ privacyNote: '在浏览器中本地运行,无需上传。',
23
+ persistenceNote: '最后草稿已保存在本地',
24
+ previewLabel: '预览',
25
+ previewEmpty: '格式化后的预览将在此显示。'
26
+ },
27
+ seo: [
28
+ {
29
+ type: 'title',
30
+ level: 2,
31
+ text: '为什么商店文案需要格式转换器'
32
+ },
33
+ {
34
+ type: 'paragraph',
35
+ html: 'Steam 商店页面编辑使用 BBCode 语法,而新闻资料包或文档网站通常使用 Markdown 或 HTML。本工具能够自动识别并同步生成其他两种格式。'
36
+ },
37
+ {
38
+ type: 'title',
39
+ level: 2,
40
+ text: '支持的标记与结构'
41
+ },
42
+ {
43
+ type: 'paragraph',
44
+ html: '支持标题、加粗、斜体、链接、列表、引用与黑幕剧透标记的转换。'
45
+ },
46
+ {
47
+ type: 'stats',
48
+ columns: 4,
49
+ items: [
50
+ { label: '输入格式', value: '3 种' },
51
+ { label: '同时输出', value: '2 种' },
52
+ { label: '列表深度', value: '支持嵌套' },
53
+ { label: '数据处理', value: '纯本地' }
54
+ ]
55
+ },
56
+ {
57
+ type: 'title',
58
+ level: 2,
59
+ text: '保留嵌套列表结构'
60
+ },
61
+ {
62
+ type: 'paragraph',
63
+ html: '通过构建轻量级语法树,确保子列表项在转换后依然精确嵌套在父级列表中。'
64
+ },
65
+ {
66
+ type: 'table',
67
+ headers: ['Steam BBCode', 'Markdown', 'HTML'],
68
+ rows: [
69
+ ['[h1]标题[/h1]', '# 标题', '&lt;h1&gt;标题&lt;/h1&gt;'],
70
+ ['[b]重点[/b]', '**重点**', '&lt;strong&gt;重点&lt;/strong&gt;'],
71
+ ['[i]备注[/i]', '*备注*', '&lt;em&gt;备注&lt;/em&gt;'],
72
+ ['[url=https://example.com]链接[/url]', '[链接](https://example.com)', '&lt;a href="https://example.com"&gt;链接&lt;/a&gt;'],
73
+ ['[list][*]第一[*]第二[/list]', '- 第一\n- 第二', '&lt;ul&gt;&lt;li&gt;第一&lt;/li&gt;&lt;li&gt;第二&lt;/li&gt;&lt;/ul&gt;']
74
+ ]
75
+ },
76
+ {
77
+ type: 'title',
78
+ level: 2,
79
+ text: 'Markdown 与 HTML 转换差异'
80
+ },
81
+ {
82
+ type: 'paragraph',
83
+ html: '当 Markdown 原生不支持下划线等样式时,转换器会自动补充内联 HTML 标签。'
84
+ },
85
+ {
86
+ type: 'tip',
87
+ title: '发布前检查建议',
88
+ html: '在发布到 Steam 或官网之前,请在预览面板中核对排版效果。'
89
+ },
90
+ {
91
+ type: 'title',
92
+ level: 2,
93
+ text: '隐私与数据安全'
94
+ },
95
+ {
96
+ type: 'paragraph',
97
+ html: '所有转换均在您的浏览器本地完成,绝不会上传任何未公开文案。'
98
+ },
99
+ {
100
+ type: 'title',
101
+ level: 2,
102
+ text: '转换说明'
103
+ },
104
+ {
105
+ type: 'proscons',
106
+ title: '转换特点',
107
+ items: [
108
+ {
109
+ pro: '结构化处理嵌套列表。',
110
+ con: '自定义非标标签需手动复核。'
111
+ }
112
+ ]
113
+ },
114
+ {
115
+ type: 'title',
116
+ level: 2,
117
+ text: '术语表'
118
+ },
119
+ {
120
+ type: 'glossary',
121
+ items: [
122
+ {
123
+ term: 'BBCode',
124
+ definition: 'Steam 商店页面使用的方括号标记语法。'
125
+ },
126
+ {
127
+ term: 'Markdown',
128
+ definition: '易读易写的轻量级标记语言。'
129
+ },
130
+ {
131
+ term: 'HTML',
132
+ definition: '网页标准的超文本标记语言。'
133
+ }
134
+ ]
135
+ }
136
+ ],
137
+ faqTitle: '常见问题解答',
138
+ faq: [
139
+ {
140
+ question: '文本会被上传到服务器吗?',
141
+ answer: '不会。转换完全在您的浏览器本地进行。'
142
+ },
143
+ {
144
+ question: '支持嵌套列表吗?',
145
+ answer: '支持。系统会先解析列表语法树再生成目标格式。'
146
+ }
147
+ ],
148
+ howTo: [
149
+ {
150
+ name: '粘贴文本',
151
+ text: '粘贴 Steam BBCode、Markdown 或 HTML 文本。'
152
+ },
153
+ {
154
+ name: '自动转换',
155
+ text: '系统将自动生成另外两种格式。'
156
+ }
157
+ ],
158
+ schemas: [
159
+ {
160
+ '@context': 'https://schema.org',
161
+ '@type': 'SoftwareApplication',
162
+ name: 'Steam BBCode, Markdown 与 HTML 转换器',
163
+ applicationCategory: 'DeveloperApplication',
164
+ operatingSystem: 'Any',
165
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'CNY' }
166
+ },
167
+ {
168
+ '@context': 'https://schema.org',
169
+ '@type': 'FAQPage',
170
+ mainEntity: [
171
+ {
172
+ '@type': 'Question',
173
+ name: '文本会被上传到服务器吗?',
174
+ acceptedAnswer: {
175
+ '@type': 'Answer',
176
+ text: 'จะไม่。转换完全在您的浏览器本地进行。'
177
+ }
178
+ }
179
+ ]
180
+ },
181
+ {
182
+ '@context': 'https://schema.org',
183
+ '@type': 'HowTo',
184
+ name: '如何转换 Steam BBCode、Markdown 与 HTML',
185
+ step: [
186
+ {
187
+ '@type': 'HowToStep',
188
+ name: '粘贴文本',
189
+ text: '粘贴 Steam BBCode、Markdown 或 HTML 文本。'
190
+ }
191
+ ]
192
+ }
193
+ ],
194
+ bibliography: bibliographyEntries
195
+ };
@@ -0,0 +1,12 @@
1
+ import type { ToolDefinition } from '../../types';
2
+ import { steamBbcodeTranslator } from './entry';
3
+
4
+ export * from './entry';
5
+ export * from './logic';
6
+
7
+ export const STEAM_BBCODE_TRANSLATOR_TOOL: ToolDefinition = {
8
+ entry: steamBbcodeTranslator,
9
+ Component: () => import('./component.astro'),
10
+ SEOComponent: () => import('./seo.astro'),
11
+ BibliographyComponent: () => import('./bibliography.astro'),
12
+ };
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { detectSyntax, getPreviewHtml, markdownToBbcode, sampleBbcode, translate, translateToAll } from './logic';
3
+
4
+ describe('steam BBCode translator', () => {
5
+ it('converts headings, inline emphasis and links to Markdown', () => {
6
+ const result = translate('[h1]Signal Lost[/h1]\n[b]Bold[/b] [url=https://example.com]Read[/url]', 'bbcode-to-output', 'markdown');
7
+ expect(result.output).toContain('# Signal Lost');
8
+ expect(result.output).toContain('**Bold**');
9
+ expect(result.output).toContain('[Read](https://example.com)');
10
+ });
11
+
12
+ it('keeps nested BBCode lists nested in Markdown and HTML', () => {
13
+ const markdown = translate('[list][*]Parent[list][*]Child[/list][*]Sibling[/list]', 'bbcode-to-output', 'markdown');
14
+ const html = translate('[list][*]Parent[list][*]Child[/list][*]Sibling[/list]', 'bbcode-to-output', 'html');
15
+ expect(markdown.output).toContain('- Parent\n - Child');
16
+ expect(html.output).toContain('<ul><li>Parent<ul><li>Child</li></ul></li><li>Sibling</li></ul>');
17
+ });
18
+
19
+ it('translates Markdown list indentation back to Steam list tags', () => {
20
+ const result = markdownToBbcode('- Parent\n - Child\n- Sibling');
21
+ expect(result.output).toBe('[list]\n[*]Parent\n[list]\n[*]Child\n[/list]\n[*]Sibling\n[/list]');
22
+ });
23
+
24
+ it('handles empty input and exposes a safe preview', () => {
25
+ expect(translate(' ', 'bbcode-to-output', 'html').stats.characters).toBe(0);
26
+ expect(getPreviewHtml(sampleBbcode, 'bbcode-to-output', 'html')).not.toContain('<script');
27
+ });
28
+
29
+ it('detects the pasted syntax and returns only the other two formats', () => {
30
+ expect(detectSyntax('[b]Steam[/b]')).toBe('bbcode');
31
+ expect(detectSyntax('# Markdown')).toBe('markdown');
32
+ expect(detectSyntax('<strong>HTML</strong>')).toBe('html');
33
+ expect(Object.keys(translateToAll(sampleBbcode).outputs).sort()).toEqual(['html', 'markdown']);
34
+ });
35
+
36
+ it('reports malformed closing tags without throwing', () => {
37
+ const result = translate('[b]Unfinished[/i]', 'bbcode-to-output', 'markdown');
38
+ expect(result.output).toContain('Unfinished');
39
+ expect(result.warnings.length).toBeGreaterThan(0);
40
+ });
41
+ });
@@ -0,0 +1,96 @@
1
+ import type { AutoTranslationResult, MarkupSyntax, OutputFormat, TranslationDirection, TranslationResult } from './types';
2
+ import { parseBbcode } from './bbcode-parser';
3
+ import { renderHtml } from './html-renderer';
4
+ import { renderMarkdown } from './markdown-renderer';
5
+ import { htmlToBbcode } from './html-parser';
6
+ import { markdownToBbcode } from './markdown-parser';
7
+
8
+ export * from './types';
9
+ export * from './storage';
10
+ export { parseBbcode } from './bbcode-parser';
11
+ export { renderMarkdown } from './markdown-renderer';
12
+ export { renderHtml } from './html-renderer';
13
+ export { htmlToBbcode } from './html-parser';
14
+ export { markdownToBbcode } from './markdown-parser';
15
+
16
+ function countBlocks(source: string): number {
17
+ return source.trim() ? source.split(/\n{2,}/).filter(Boolean).length : 0;
18
+ }
19
+
20
+ export function convertBbcode(source: string, format: OutputFormat): TranslationResult {
21
+ const parsed = parseBbcode(source);
22
+ const output = format === 'html' ? renderHtml(parsed.nodes) : renderMarkdown(parsed.nodes);
23
+ return {
24
+ output,
25
+ warnings: parsed.warnings,
26
+ stats: { characters: output.length, tags: parsed.tagCount, blocks: countBlocks(output) }
27
+ };
28
+ }
29
+
30
+ export function detectSyntax(source: string): MarkupSyntax {
31
+ const value = source.trim();
32
+ if (!value) return 'markdown';
33
+ if (/<(?:h[1-6]|p|strong|em|b|i|ul|ol|li|a|br|blockquote|code)\b[^>]*>/i.test(value)) return 'html';
34
+ if (/\[(?:\/?(?:h[1-3]|b|strong|i|em|u|s|strike|url|code|quote|spoiler|list|olist)|\*)[^\]]*\]/i.test(value)) return 'bbcode';
35
+ return 'markdown';
36
+ }
37
+
38
+ export function translate(source: string, direction: TranslationDirection, format: OutputFormat): TranslationResult {
39
+ if (!source.trim()) return { output: '', warnings: [], stats: { characters: 0, tags: 0, blocks: 0 } };
40
+ if (direction === 'bbcode-to-output') return convertBbcode(source, format);
41
+ if (format === 'html') return htmlToBbcode(source);
42
+ return markdownToBbcode(source);
43
+ }
44
+
45
+ function translateNonBbcode(source: string, detected: MarkupSyntax): AutoTranslationResult {
46
+ const bbcode = detected === 'html' ? htmlToBbcode(source) : markdownToBbcode(source);
47
+ const secondary = detected === 'html'
48
+ ? { markdown: convertBbcode(bbcode.output, 'markdown') }
49
+ : { html: convertBbcode(bbcode.output, 'html') };
50
+
51
+ return {
52
+ detected,
53
+ outputs: {
54
+ bbcode,
55
+ ...secondary
56
+ }
57
+ };
58
+ }
59
+
60
+ export function translateToAll(source: string): AutoTranslationResult {
61
+ const detected = detectSyntax(source);
62
+ if (!source.trim()) return { detected, outputs: {} };
63
+ if (detected === 'bbcode') {
64
+ return {
65
+ detected,
66
+ outputs: {
67
+ markdown: convertBbcode(source, 'markdown'),
68
+ html: convertBbcode(source, 'html')
69
+ }
70
+ };
71
+ }
72
+ return translateNonBbcode(source, detected);
73
+ }
74
+
75
+ export function getPreviewHtml(source: string, direction: TranslationDirection, format: OutputFormat): string {
76
+ if (!source.trim()) return '';
77
+ if (direction === 'bbcode-to-output') {
78
+ const result = translate(source, direction, format);
79
+ return format === 'html' ? result.output : convertBbcode(markdownToBbcode(result.output).output, 'html').output;
80
+ }
81
+ return convertBbcode(translate(source, direction, format).output, 'html').output;
82
+ }
83
+
84
+ export function getAutoPreviewHtml(source: string): string {
85
+ if (!source.trim()) return '';
86
+ const detected = detectSyntax(source);
87
+ if (detected === 'bbcode') return convertBbcode(source, 'html').output;
88
+ const bbcode = detected === 'html' ? htmlToBbcode(source).output : markdownToBbcode(source).output;
89
+ return convertBbcode(bbcode, 'html').output;
90
+ }
91
+
92
+ export const sampleBbcode = `[b]The Last Lantern[/b]\n\nA hand crafted survival adventure about a flooded city, a fragile airship, and the people you choose to rescue.\n\n[list][*]Scavenge abandoned districts[*]Upgrade your workshop[list][*]Solar engine[*]Deep water hull[/list][*]Make difficult choices[/list]\n\n[url=https://example.com/presskit]Read the press kit[/url]`;
93
+
94
+ export const sampleMarkdown = `# The Last Lantern\n\n**A hand crafted survival adventure**\nExplore a flooded city, rebuild your airship, and choose who gets a place on board.\n\n- Scavenge abandoned districts\n- Upgrade your workshop\n - Solar engine\n - Deep water hull\n- Make difficult choices\n\n[Read the press kit](https://example.com/presskit)`;
95
+
96
+ export const sampleHtml = `<h1>The Last Lantern</h1><p><strong>A hand crafted survival adventure</strong><br>Explore a flooded city, rebuild your airship, and choose who gets a place on board.</p><ul><li>Scavenge abandoned districts</li><li>Upgrade your workshop<ul><li>Solar engine</li><li>Deep water hull</li></ul></li><li>Make difficult choices</li></ul><a href="https://example.com/presskit">Read the press kit</a>`;
@@ -0,0 +1,115 @@
1
+ import type { TranslationResult } from './types';
2
+
3
+ export function convertInlineMarkdown(value: string): string {
4
+ let output = value;
5
+ output = output.replace(/`([^`\n]+)`/g, '[code]$1[/code]');
6
+ output = output.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1');
7
+ output = output.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '[url=$2]$1[/url]');
8
+ output = output.replace(/\*\*([^*\n]+)\*\*/g, '[b]$1[/b]');
9
+ output = output.replace(/__([^_\n]+)__/g, '[b]$1[/b]');
10
+ output = output.replace(/~~([^~\n]+)~~/g, '[s]$1[/s]');
11
+ output = output.replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g, '[i]$1[/i]');
12
+ output = output.replace(/(?<!_)_([^_\n]+)_(?!_)/g, '[i]$1[/i]');
13
+ return output;
14
+ }
15
+
16
+ function countBlocks(source: string): number {
17
+ return source.trim() ? source.split(/\n{2,}/).filter(Boolean).length : 0;
18
+ }
19
+
20
+ interface ListState {
21
+ level: number;
22
+ orderedLevel: number;
23
+ }
24
+
25
+ function adjustListDepth(state: ListState, targetLevel: number, isOrdered: boolean, output: string[]): ListState {
26
+ let { level, orderedLevel } = state;
27
+ while (level > targetLevel) {
28
+ output.push(level === orderedLevel ? '[/olist]' : '[/list]');
29
+ if (level === orderedLevel) orderedLevel -= 1;
30
+ level -= 1;
31
+ }
32
+ while (level < targetLevel) {
33
+ level += 1;
34
+ if (isOrdered) orderedLevel = level;
35
+ output.push(isOrdered ? '[olist]' : '[list]');
36
+ }
37
+ return { level, orderedLevel };
38
+ }
39
+
40
+ function processListLine(state: ListState, listMatch: RegExpExecArray, output: string[]): ListState {
41
+ const indentStr = listMatch[1] ?? '';
42
+ const marker = listMatch[2] ?? '-';
43
+ const textContent = listMatch[3] ?? '';
44
+ const targetLevel = Math.floor(indentStr.length / 2);
45
+ const isOrdered = /\d+\./.test(marker);
46
+ const newState = adjustListDepth(state, targetLevel, isOrdered, output);
47
+ output.push(`[*]${convertInlineMarkdown(textContent)}`);
48
+ return newState;
49
+ }
50
+
51
+ function closeAllLists(state: ListState, output: string[]): ListState {
52
+ let { level, orderedLevel } = state;
53
+ while (level >= 0) {
54
+ output.push(level === orderedLevel ? '[/olist]' : '[/list]');
55
+ if (level === orderedLevel) orderedLevel -= 1;
56
+ level -= 1;
57
+ }
58
+ return { level: -1, orderedLevel: -1 };
59
+ }
60
+
61
+ function processCodeBlock(lines: string[], lineIndex: number, output: string[], warnings: string[]): number {
62
+ const codeLines: string[] = [];
63
+ let next = lineIndex + 1;
64
+ while (next < lines.length) {
65
+ const nextLine = lines[next];
66
+ if (nextLine && /^```/.test(nextLine)) break;
67
+ if (nextLine !== undefined) codeLines.push(nextLine);
68
+ next += 1;
69
+ }
70
+ if (next >= lines.length) warnings.push('An unclosed Markdown code fence was closed automatically.');
71
+ output.push(`[code]${codeLines.join('\n')}[/code]`);
72
+ return next;
73
+ }
74
+
75
+ function parseMarkdownLine(line: string, output: string[]): boolean {
76
+ const heading = /^(#{1,3})\s+(.+)$/.exec(line);
77
+ if (heading) {
78
+ const level = heading[1]?.length ?? 1;
79
+ output.push(`[h${level}]${convertInlineMarkdown(heading[2] ?? '')}[/h${level}]`);
80
+ return true;
81
+ }
82
+ const quote = /^>\s?(.*)$/.exec(line);
83
+ if (quote) {
84
+ output.push(`[quote]${convertInlineMarkdown(quote[1] ?? '')}[/quote]`);
85
+ return true;
86
+ }
87
+ return false;
88
+ }
89
+
90
+ export function markdownToBbcode(source: string): TranslationResult {
91
+ const lines = source.replace(/\r/g, '').split('\n');
92
+ const output: string[] = [];
93
+ const warnings: string[] = [];
94
+ let state: ListState = { level: -1, orderedLevel: -1 };
95
+
96
+ for (let idx = 0; idx < lines.length; idx += 1) {
97
+ const line = lines[idx] ?? '';
98
+ const listMatch = /^(\s*)([-+*]|\d+\.)\s+(.+)$/.exec(line);
99
+ if (listMatch) {
100
+ state = processListLine(state, listMatch, output);
101
+ continue;
102
+ }
103
+ state = closeAllLists(state, output);
104
+ if (!line.trim()) {
105
+ output.push('');
106
+ } else if (/^```/.test(line)) {
107
+ idx = processCodeBlock(lines, idx, output, warnings);
108
+ } else if (!parseMarkdownLine(line, output)) {
109
+ output.push(convertInlineMarkdown(line));
110
+ }
111
+ }
112
+ closeAllLists(state, output);
113
+ const result = output.join('\n').replace(/\n{3,}/g, '\n\n').trim();
114
+ return { output: result, warnings, stats: { characters: result.length, tags: (result.match(/\[[a-z*]/gi) ?? []).length, blocks: countBlocks(result) } };
115
+ }