@jjlmoya/utils-games-development 1.53.0 → 1.54.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 +2 -0
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- 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/tools.ts +2 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Node, TagNode } from './types';
|
|
2
|
+
|
|
3
|
+
function escapeMarkdown(value: string): string {
|
|
4
|
+
return value.replace(/([\\`*_{}[\]()#+!|>])/g, '\\$1');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function escapeMarkdownUrl(value: string): string {
|
|
8
|
+
return value.replace(/[()\\ ]/g, (character) => `\\${character}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function cleanInline(value: string): string {
|
|
12
|
+
return value.replace(/[ \t]+\n/g, '\n').replace(/\n[ \t]+/g, '\n');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function indent(value: string): string {
|
|
16
|
+
return value.split('\n').map((line) => line ? ` ${line}` : line).join('\n');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function inlineMarkdown(nodes: Node[]): string {
|
|
20
|
+
return cleanInline(nodes.map((node) => {
|
|
21
|
+
if (node.kind === 'text') return escapeMarkdown(node.value);
|
|
22
|
+
return renderMarkdownNode(node, true);
|
|
23
|
+
}).join(''));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function renderMarkdownList(node: TagNode, ordered: boolean): string {
|
|
27
|
+
const items = node.children.filter((child): child is TagNode => child.kind === 'tag' && child.name === '*');
|
|
28
|
+
return items.map((item, index) => {
|
|
29
|
+
const content = item.children.map((child) => {
|
|
30
|
+
if (child.kind === 'tag' && (child.name === 'list' || child.name === 'olist')) {
|
|
31
|
+
return `\n${indent(renderMarkdownList(child, child.name === 'olist'))}`;
|
|
32
|
+
}
|
|
33
|
+
if (child.kind === 'text') return escapeMarkdown(child.value);
|
|
34
|
+
return renderMarkdownNode(child, true);
|
|
35
|
+
}).join('').trim();
|
|
36
|
+
return `${ordered ? `${index + 1}.` : '-'} ${content}`;
|
|
37
|
+
}).join('\n');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderCodeNode(node: TagNode, inline: boolean): string {
|
|
41
|
+
const inner = node.children.map((child) => child.kind === 'text' ? child.value : renderMarkdownNode(child, true)).join('');
|
|
42
|
+
return inline ? `\`${inner}\`` : `\`\`\`\n${inner}\n\`\`\`\n\n`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function renderQuoteNode(node: TagNode): string {
|
|
46
|
+
return node.children.map((child) => inlineMarkdown([child])).join('').split('\n').map((line) => `> ${line}`).join('\n') + '\n\n';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function renderMarkdownBasicInline(name: string, content: string): string | null {
|
|
50
|
+
if (name === 'b' || name === 'strong') return `**${content}**`;
|
|
51
|
+
if (name === 'i' || name === 'em') return `*${content}*`;
|
|
52
|
+
if (name === 'u') return `<u>${content}</u>`;
|
|
53
|
+
if (name === 's' || name === 'strike') return `~~${content}~~`;
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function renderMarkdownInlineNode(name: string, content: string, value?: string): string {
|
|
58
|
+
const basic = renderMarkdownBasicInline(name, content);
|
|
59
|
+
if (basic !== null) return basic;
|
|
60
|
+
if (name === 'url') return value ? `[${content || escapeMarkdown(value)}](${escapeMarkdownUrl(value)})` : content;
|
|
61
|
+
return content;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function renderMarkdownHeadingOrBlock(name: string, content: string, node: TagNode, inline: boolean): string | null {
|
|
65
|
+
if (name === 'h1') return `# ${content.trim()}\n\n`;
|
|
66
|
+
if (name === 'h2') return `## ${content.trim()}\n\n`;
|
|
67
|
+
if (name === 'h3') return `### ${content.trim()}\n\n`;
|
|
68
|
+
if (name === 'code') return renderCodeNode(node, inline);
|
|
69
|
+
if (name === 'quote') return renderQuoteNode(node);
|
|
70
|
+
if (name === 'spoiler') return `<details><summary>Spoiler</summary>${content}</details>`;
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function renderMarkdownNode(node: TagNode, inline = false): string {
|
|
75
|
+
const content = inlineMarkdown(node.children);
|
|
76
|
+
const headingOrBlock = renderMarkdownHeadingOrBlock(node.name, content, node, inline);
|
|
77
|
+
if (headingOrBlock !== null) return headingOrBlock;
|
|
78
|
+
if (node.name === 'list') return `${renderMarkdownList(node, false)}\n\n`;
|
|
79
|
+
if (node.name === 'olist') return `${renderMarkdownList(node, true)}\n\n`;
|
|
80
|
+
return renderMarkdownInlineNode(node.name, content, node.value);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function renderMarkdown(nodes: Node[]): string {
|
|
84
|
+
return nodes.map((node) => node.kind === 'text' ? escapeMarkdown(node.value) : renderMarkdownNode(node)).join('').replace(/\n{3,}/g, '\n\n').trim();
|
|
85
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { steamBbcodeTranslator } from './entry';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props;
|
|
11
|
+
const loader = steamBbcodeTranslator.i18n[locale] ?? steamBbcodeTranslator.i18n.en;
|
|
12
|
+
const content = loader ? await loader() : null;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
{content && content.seo && content.seo.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
.bbcode-root {
|
|
2
|
+
--bb-ink: #211e2d;
|
|
3
|
+
--bb-muted: #77728a;
|
|
4
|
+
--bb-line: #dcd8e7;
|
|
5
|
+
--bb-panel: #fffdfb;
|
|
6
|
+
--bb-soft: #f4f0f7;
|
|
7
|
+
--bb-accent: #724ee8;
|
|
8
|
+
--bb-accent-deep: #4c2fba;
|
|
9
|
+
--bb-cyan: #20a8b5;
|
|
10
|
+
--bb-warm: #ed8a52;
|
|
11
|
+
|
|
12
|
+
color: var(--bb-ink);
|
|
13
|
+
margin: 0 auto;
|
|
14
|
+
max-width: 1120px;
|
|
15
|
+
width: 100%;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.theme-dark .bbcode-root {
|
|
19
|
+
--bb-ink: #f4f1fb;
|
|
20
|
+
--bb-muted: #aaa4bb;
|
|
21
|
+
--bb-line: #342e47;
|
|
22
|
+
--bb-panel: #171422;
|
|
23
|
+
--bb-soft: #211c30;
|
|
24
|
+
--bb-accent: #a68cff;
|
|
25
|
+
--bb-accent-deep: #c0b0ff;
|
|
26
|
+
--bb-cyan: #63d2dc;
|
|
27
|
+
--bb-warm: #f3a06d;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.bbcode-root * {
|
|
31
|
+
box-sizing: border-box;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.bbcode-root button,
|
|
35
|
+
.bbcode-root textarea {
|
|
36
|
+
font: inherit;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.bbcode-header,
|
|
40
|
+
.bbcode-input-card,
|
|
41
|
+
.bbcode-output-card,
|
|
42
|
+
.bbcode-preview-card {
|
|
43
|
+
background: var(--bb-panel);
|
|
44
|
+
border: 1px solid var(--bb-line);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.bbcode-header {
|
|
48
|
+
align-items: center;
|
|
49
|
+
border-radius: 18px 18px 0 0;
|
|
50
|
+
display: flex;
|
|
51
|
+
justify-content: space-between;
|
|
52
|
+
gap: 1rem;
|
|
53
|
+
padding: 1rem 1.2rem;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.bbcode-title-row {
|
|
57
|
+
align-items: center;
|
|
58
|
+
display: flex;
|
|
59
|
+
gap: 0.75rem;
|
|
60
|
+
min-width: 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.bbcode-mark {
|
|
64
|
+
align-items: center;
|
|
65
|
+
background: var(--bb-soft);
|
|
66
|
+
border: 1px solid var(--bb-line);
|
|
67
|
+
border-radius: 8px;
|
|
68
|
+
color: var(--bb-accent);
|
|
69
|
+
display: inline-flex;
|
|
70
|
+
font-size: 0.9rem;
|
|
71
|
+
font-weight: 900;
|
|
72
|
+
height: 36px;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
width: 58px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.bbcode-kicker {
|
|
78
|
+
color: var(--bb-accent);
|
|
79
|
+
display: block;
|
|
80
|
+
font-size: 0.62rem;
|
|
81
|
+
font-weight: 900;
|
|
82
|
+
letter-spacing: 0.14em;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.bbcode-title-row strong,
|
|
86
|
+
.bbcode-draft-note {
|
|
87
|
+
color: var(--bb-muted);
|
|
88
|
+
display: block;
|
|
89
|
+
font-size: 0.72rem;
|
|
90
|
+
font-weight: 600;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.bbcode-draft-note {
|
|
94
|
+
text-align: right;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.bbcode-input-card {
|
|
98
|
+
border-top: 0;
|
|
99
|
+
padding: 1.2rem;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.bbcode-section-head,
|
|
103
|
+
.bbcode-output-heading,
|
|
104
|
+
.bbcode-output-card-head,
|
|
105
|
+
.bbcode-input-footer {
|
|
106
|
+
align-items: flex-start;
|
|
107
|
+
display: flex;
|
|
108
|
+
justify-content: space-between;
|
|
109
|
+
gap: 1rem;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.bbcode-section-head {
|
|
113
|
+
margin-bottom: 0.7rem;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.bbcode-section-head h2,
|
|
117
|
+
.bbcode-output-heading h2 {
|
|
118
|
+
display: inline;
|
|
119
|
+
font-size: 1rem;
|
|
120
|
+
margin: 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.bbcode-step {
|
|
124
|
+
color: var(--bb-warm);
|
|
125
|
+
font-size: 0.68rem;
|
|
126
|
+
font-weight: 900;
|
|
127
|
+
margin-right: 0.45rem;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.bbcode-detected {
|
|
131
|
+
background: var(--bb-soft);
|
|
132
|
+
border-radius: 999px;
|
|
133
|
+
color: var(--bb-accent-deep);
|
|
134
|
+
font-size: 0.66rem;
|
|
135
|
+
padding: 0.3rem 0.55rem;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.bbcode-input-card textarea {
|
|
139
|
+
background: var(--bb-soft);
|
|
140
|
+
border: 1px solid var(--bb-line);
|
|
141
|
+
border-radius: 9px;
|
|
142
|
+
color: var(--bb-ink);
|
|
143
|
+
display: block;
|
|
144
|
+
font-size: 0.8rem;
|
|
145
|
+
line-height: 1.65;
|
|
146
|
+
min-height: 240px;
|
|
147
|
+
padding: 1rem;
|
|
148
|
+
resize: vertical;
|
|
149
|
+
width: 100%;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.bbcode-input-card textarea:focus {
|
|
153
|
+
border-color: var(--bb-accent);
|
|
154
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--bb-accent) 18%, transparent);
|
|
155
|
+
outline: 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.bbcode-input-footer {
|
|
159
|
+
align-items: center;
|
|
160
|
+
margin-top: 0.6rem;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.bbcode-input-footer p {
|
|
164
|
+
color: var(--bb-muted);
|
|
165
|
+
font-size: 0.7rem;
|
|
166
|
+
line-height: 1.4;
|
|
167
|
+
margin: 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.bbcode-input-meta {
|
|
171
|
+
align-items: center;
|
|
172
|
+
color: var(--bb-muted);
|
|
173
|
+
display: flex;
|
|
174
|
+
flex-wrap: wrap;
|
|
175
|
+
font-size: 0.68rem;
|
|
176
|
+
gap: 0.8rem;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.bbcode-input-meta strong {
|
|
180
|
+
color: var(--bb-ink);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.bbcode-clear-button,
|
|
184
|
+
.bbcode-icon-button {
|
|
185
|
+
background: transparent;
|
|
186
|
+
border: 1px solid var(--bb-line);
|
|
187
|
+
border-radius: 6px;
|
|
188
|
+
color: var(--bb-ink);
|
|
189
|
+
cursor: pointer;
|
|
190
|
+
font-size: 0.68rem;
|
|
191
|
+
font-weight: 800;
|
|
192
|
+
padding: 0.38rem 0.6rem;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.bbcode-clear-button:hover,
|
|
196
|
+
.bbcode-icon-button:hover {
|
|
197
|
+
border-color: var(--bb-accent);
|
|
198
|
+
color: var(--bb-accent-deep);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.bbcode-output-section {
|
|
202
|
+
background: var(--bb-soft);
|
|
203
|
+
border-left: 1px solid var(--bb-line);
|
|
204
|
+
border-right: 1px solid var(--bb-line);
|
|
205
|
+
padding: 1rem 1.2rem;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.bbcode-output-heading {
|
|
209
|
+
align-items: center;
|
|
210
|
+
margin-bottom: 0.7rem;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.bbcode-output-grid {
|
|
214
|
+
display: grid;
|
|
215
|
+
gap: 0.8rem;
|
|
216
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.bbcode-output-card {
|
|
220
|
+
border-radius: 10px;
|
|
221
|
+
min-width: 0;
|
|
222
|
+
padding: 0.8rem;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.bbcode-output-card-head {
|
|
226
|
+
align-items: center;
|
|
227
|
+
margin-bottom: 0.55rem;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.bbcode-output-card h3 {
|
|
231
|
+
font-size: 0.78rem;
|
|
232
|
+
margin: 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.bbcode-output-card pre {
|
|
236
|
+
background: var(--bb-soft);
|
|
237
|
+
border: 1px solid var(--bb-line);
|
|
238
|
+
border-radius: 7px;
|
|
239
|
+
color: var(--bb-ink);
|
|
240
|
+
font-size: 0.72rem;
|
|
241
|
+
line-height: 1.6;
|
|
242
|
+
margin: 0;
|
|
243
|
+
min-height: 180px;
|
|
244
|
+
max-height: 330px;
|
|
245
|
+
overflow: auto;
|
|
246
|
+
padding: 0.8rem;
|
|
247
|
+
white-space: pre-wrap;
|
|
248
|
+
overflow-wrap: anywhere;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.bbcode-preview-card {
|
|
252
|
+
border-radius: 0 0 18px 18px;
|
|
253
|
+
border-top: 0;
|
|
254
|
+
padding: 1rem 1.2rem 1.2rem;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.bbcode-preview-surface {
|
|
258
|
+
background: var(--bb-soft);
|
|
259
|
+
border: 1px solid var(--bb-line);
|
|
260
|
+
border-radius: 9px;
|
|
261
|
+
color: var(--bb-ink);
|
|
262
|
+
font-size: 0.82rem;
|
|
263
|
+
line-height: 1.6;
|
|
264
|
+
min-height: 110px;
|
|
265
|
+
padding: 1rem;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.bbcode-preview-surface h1,
|
|
269
|
+
.bbcode-preview-surface h2,
|
|
270
|
+
.bbcode-preview-surface h3 {
|
|
271
|
+
line-height: 1.15;
|
|
272
|
+
margin: 0 0 0.6rem;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.bbcode-preview-surface h1 {
|
|
276
|
+
font-size: 1.35rem;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.bbcode-preview-surface h2 {
|
|
280
|
+
font-size: 1.12rem;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.bbcode-preview-surface h3 {
|
|
284
|
+
font-size: 0.98rem;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.bbcode-preview-surface p {
|
|
288
|
+
margin: 0.3rem 0;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.bbcode-preview-surface ul,
|
|
292
|
+
.bbcode-preview-surface ol {
|
|
293
|
+
margin: 0.45rem 0;
|
|
294
|
+
padding-left: 1.3rem;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.bbcode-preview-surface li {
|
|
298
|
+
margin: 0.15rem 0;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.bbcode-preview-surface blockquote {
|
|
302
|
+
border-left: 3px solid var(--bb-accent);
|
|
303
|
+
color: var(--bb-muted);
|
|
304
|
+
margin: 0.6rem 0;
|
|
305
|
+
padding-left: 0.7rem;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.bbcode-preview-surface a {
|
|
309
|
+
color: var(--bb-accent-deep);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.bbcode-preview-surface code,
|
|
313
|
+
.bbcode-preview-surface pre {
|
|
314
|
+
background: var(--bb-panel);
|
|
315
|
+
border: 1px solid var(--bb-line);
|
|
316
|
+
border-radius: 5px;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.bbcode-preview-surface code {
|
|
320
|
+
padding: 0.1rem 0.25rem;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.bbcode-preview-surface pre {
|
|
324
|
+
overflow: auto;
|
|
325
|
+
padding: 0.6rem;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.bbcode-preview-surface pre code {
|
|
329
|
+
border: 0;
|
|
330
|
+
padding: 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.bbcode-preview-surface p:first-child:last-child {
|
|
334
|
+
color: var(--bb-muted);
|
|
335
|
+
margin: 0;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.bbcode-sr-only {
|
|
339
|
+
height: 1px;
|
|
340
|
+
margin: -1px;
|
|
341
|
+
overflow: hidden;
|
|
342
|
+
position: absolute;
|
|
343
|
+
width: 1px;
|
|
344
|
+
clip-path: inset(50%);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.bbcode-root button:focus-visible,
|
|
348
|
+
.bbcode-root textarea:focus-visible {
|
|
349
|
+
outline: 3px solid color-mix(in srgb, var(--bb-cyan) 45%, transparent);
|
|
350
|
+
outline-offset: 2px;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
@media (max-width: 700px) {
|
|
354
|
+
.bbcode-header,
|
|
355
|
+
.bbcode-input-footer {
|
|
356
|
+
align-items: flex-start;
|
|
357
|
+
flex-direction: column;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.bbcode-draft-note {
|
|
361
|
+
text-align: left;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.bbcode-input-meta {
|
|
365
|
+
width: 100%;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.bbcode-output-grid {
|
|
369
|
+
grid-template-columns: 1fr;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
@media (max-width: 460px) {
|
|
374
|
+
.bbcode-header,
|
|
375
|
+
.bbcode-input-card,
|
|
376
|
+
.bbcode-output-section,
|
|
377
|
+
.bbcode-preview-card {
|
|
378
|
+
padding-left: 0.85rem;
|
|
379
|
+
padding-right: 0.85rem;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.bbcode-input-meta {
|
|
383
|
+
justify-content: space-between;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
@media (prefers-reduced-motion: reduce) {
|
|
388
|
+
.bbcode-root * {
|
|
389
|
+
scroll-behavior: auto;
|
|
390
|
+
transition-duration: 0.01ms;
|
|
391
|
+
}
|
|
392
|
+
}
|