flet-box 0.0.0-stage → 1.0.2
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/LICENSE +21 -0
- package/README.md +190 -2
- package/bin/cli.js +394 -0
- package/bin/commands/buildAndroid.js +200 -0
- package/bin/commands/buildIOS.js +237 -0
- package/bin/commands/create.js +406 -0
- package/bin/commands/createBundle.js +375 -0
- package/bin/commands/createComponent.js +115 -0
- package/bin/commands/createScreen.js +161 -0
- package/bin/commands/killServer.js +107 -0
- package/bin/commands/package.js +131 -0
- package/bin/commands/release.js +189 -0
- package/bin/commands/runBundle.js +62 -0
- package/bin/commands/runCloudflare.js +89 -0
- package/bin/commands/runNative.js +109 -0
- package/bin/commands/runServer.js +390 -0
- package/bin/ota/loader.js +361 -0
- package/bin/utils/bundleConfig.js +178 -0
- package/bin/utils/capacitor.js +137 -0
- package/bin/utils/colors.js +215 -0
- package/bin/utils/helpers.js +80 -0
- package/bin/utils/spinner.js +96 -0
- package/bin/utils/templates.js +1824 -0
- package/bin/utils/version.js +108 -0
- package/package.json +58 -4
- package/src/animations/AnimatedBox.js +607 -0
- package/src/animations/AnimatedText.js +105 -0
- package/src/animations/MatrixRain.js +220 -0
- package/src/animations/ParallaxBox.js +369 -0
- package/src/animations/animate.js +332 -0
- package/src/animations/index.js +25 -0
- package/src/app.js +41 -0
- package/src/assets/icon-1024-maskable.png +0 -0
- package/src/assets/icon-192-maskable.png +0 -0
- package/src/assets/icon-192.png +0 -0
- package/src/assets/icon-512-maskable.png +0 -0
- package/src/assets/icon-512.png +0 -0
- package/src/assets/logo.png +0 -0
- package/src/core/App.js +44 -0
- package/src/core/hmr-client.js +142 -0
- package/src/core/index.js +34 -0
- package/src/core/pwa.js +100 -0
- package/src/core/runApp.js +430 -0
- package/src/create-index.sh +237 -0
- package/src/fonts/MaterialIcons-Regular.woff2 +0 -0
- package/src/fonts/icons.css +23 -0
- package/src/index.d.ts +1930 -0
- package/src/index.js +288 -0
- package/src/navigations/AdaptiveScaffold.js +149 -0
- package/src/navigations/AppBar.js +331 -0
- package/src/navigations/BottomNavigation.js +298 -0
- package/src/navigations/CollapsibleSideBar.js +139 -0
- package/src/navigations/Drawer.js +333 -0
- package/src/navigations/DrawerItem.js +260 -0
- package/src/navigations/Router.js +475 -0
- package/src/navigations/Scaffold.js +537 -0
- package/src/navigations/Tabs.js +384 -0
- package/src/navigations/index.js +57 -0
- package/src/services/RamStore.js +213 -0
- package/src/services/Session.js +310 -0
- package/src/services/Storage.js +309 -0
- package/src/services/http.js +350 -0
- package/src/services/index.js +57 -0
- package/src/tools/animation.js +42 -0
- package/src/tools/array.js +95 -0
- package/src/tools/border.js +28 -0
- package/src/tools/clipboard.js +104 -0
- package/src/tools/color.js +329 -0
- package/src/tools/createList.js +215 -0
- package/src/tools/delay.js +55 -0
- package/src/tools/device.js +138 -0
- package/src/tools/dict.js +297 -0
- package/src/tools/dimensions.js +118 -0
- package/src/tools/filter.js +41 -0
- package/src/tools/flex.js +26 -0
- package/src/tools/gradient.js +34 -0
- package/src/tools/grid.js +57 -0
- package/src/tools/index.js +38 -0
- package/src/tools/mapList.js +107 -0
- package/src/tools/margin.js +93 -0
- package/src/tools/memo.js +60 -0
- package/src/tools/os.js +104 -0
- package/src/tools/padding.js +93 -0
- package/src/tools/print.js +69 -0
- package/src/tools/random.js +299 -0
- package/src/tools/ref.js +70 -0
- package/src/tools/rgba.js +4 -0
- package/src/tools/shadow.js +23 -0
- package/src/tools/string.js +85 -0
- package/src/tools/time.js +674 -0
- package/src/tools/transform.js +68 -0
- package/src/tools/transition.js +48 -0
- package/src/tools/useState.js +268 -0
- package/src/tools/uuid.js +42 -0
- package/src/utils/TextInputValidator.js +256 -0
- package/src/utils/composeUpdate.js +65 -0
- package/src/utils/getWidgetProps.js +35 -0
- package/src/utils/index.js +76 -0
- package/src/utils/markdownParser.js +368 -0
- package/src/utils/mediaTime.js +79 -0
- package/src/utils/navigation.js +263 -0
- package/src/utils/scriptInjector.js +73 -0
- package/src/utils/stackPosition.js +31 -0
- package/src/utils/stopWebRefresh.js +77 -0
- package/src/utils/styleInjector.js +38 -0
- package/src/utils/syntaxHighlight.js +339 -0
- package/src/utils/themes.js +347 -0
- package/src/utils/units.js +87 -0
- package/src/utils/visualEffects.js +169 -0
- package/src/widget-factory/addChildren.js +114 -0
- package/src/widget-factory/assignProps.js +256 -0
- package/src/widget-factory/createWidget.js +28 -0
- package/src/widget-factory/effects.js +132 -0
- package/src/widget-factory/index.js +7 -0
- package/src/widget-factory/lifecycle.js +210 -0
- package/src/widget-factory/makeParentable.js +89 -0
- package/src/widget-factory/pixTool.js +40 -0
- package/src/widget-factory/processProps.js +122 -0
- package/src/widget-factory/reactivity.js +98 -0
- package/src/widget-factory/remTool.js +34 -0
- package/src/widget-factory/stackPosition.js +87 -0
- package/src/widget-factory/stringTool.js +26 -0
- package/src/widget-factory/tools.js +249 -0
- package/src/widget-factory/translateProps.js +1049 -0
- package/src/widget-factory/warnings.js +284 -0
- package/src/widget-factory/widgetFactory.js +200 -0
- package/src/widgets/Accordion.js +562 -0
- package/src/widgets/AdMob.js +315 -0
- package/src/widgets/AdSense.js +268 -0
- package/src/widgets/AlertDialog.js +162 -0
- package/src/widgets/Audio.js +311 -0
- package/src/widgets/Avatar.js +210 -0
- package/src/widgets/Badge.js +189 -0
- package/src/widgets/BottomSheet.js +287 -0
- package/src/widgets/Button.js +247 -0
- package/src/widgets/Card.js +33 -0
- package/src/widgets/Carousel.js +301 -0
- package/src/widgets/Chart.js +632 -0
- package/src/widgets/Checkbox.js +106 -0
- package/src/widgets/Chip.js +153 -0
- package/src/widgets/CircularBar.js +352 -0
- package/src/widgets/CircularChart.js +549 -0
- package/src/widgets/CodeViewer.js +236 -0
- package/src/widgets/Column.js +29 -0
- package/src/widgets/Container.js +31 -0
- package/src/widgets/DataTable.js +257 -0
- package/src/widgets/Divider.js +93 -0
- package/src/widgets/DraggBox.js +115 -0
- package/src/widgets/Dropdown.js +505 -0
- package/src/widgets/DroppBox.js +180 -0
- package/src/widgets/FloatingActionButton.js +91 -0
- package/src/widgets/GridView.js +20 -0
- package/src/widgets/Icon.js +33 -0
- package/src/widgets/Image.js +15 -0
- package/src/widgets/Input.js +565 -0
- package/src/widgets/Inspector.js +254 -0
- package/src/widgets/InstallButton.js +110 -0
- package/src/widgets/ListTile.js +224 -0
- package/src/widgets/ListView.js +503 -0
- package/src/widgets/Markdown.js +1236 -0
- package/src/widgets/Modal.js +496 -0
- package/src/widgets/Pagination.js +195 -0
- package/src/widgets/ProgressBar.js +178 -0
- package/src/widgets/QRCode.js +467 -0
- package/src/widgets/Radio.js +132 -0
- package/src/widgets/Rating.js +261 -0
- package/src/widgets/Row.js +29 -0
- package/src/widgets/Skeleton.js +243 -0
- package/src/widgets/Slider.js +527 -0
- package/src/widgets/SnackBar.js +227 -0
- package/src/widgets/Stack.js +29 -0
- package/src/widgets/Stepper.js +296 -0
- package/src/widgets/Switch.js +102 -0
- package/src/widgets/Text.js +98 -0
- package/src/widgets/Tooltip.js +267 -0
- package/src/widgets/TreeView.js +370 -0
- package/src/widgets/Video.js +92 -0
- package/src/widgets/index.js +52 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file composeUpdate.js
|
|
3
|
+
* @description
|
|
4
|
+
* Composes a widget's own prop-application logic with the factory's reactive
|
|
5
|
+
* `update` instead of overwriting it.
|
|
6
|
+
*
|
|
7
|
+
* Widgets such as Accordion, Slider and Dropdown keep widget state (currentValue,
|
|
8
|
+
* currentOptions, …) in a closure and need a bespoke `update` to apply it. The
|
|
9
|
+
* naive implementation `container.update = update` destroys the factory's update,
|
|
10
|
+
* which re-applies styles, events, effects and children — so generic calls like
|
|
11
|
+
* `slider.update({ opacity: .5 })` silently stopped working.
|
|
12
|
+
*
|
|
13
|
+
* `composeUpdate` splits every `update(newProps)` call in two:
|
|
14
|
+
* - keys owned by the widget (the `widgetPropKeys` set) → `applyWidgetProps`
|
|
15
|
+
* - everything else → the factory's original `update`
|
|
16
|
+
*
|
|
17
|
+
* Widget-owned keys are also stripped from `_originalProps`, because the factory
|
|
18
|
+
* merges them back on every update: a stale construction-time value (say
|
|
19
|
+
* `border: "1px solid red"`) would be re-applied as a style and silently clobber
|
|
20
|
+
* a later widget-level change of the same prop.
|
|
21
|
+
*
|
|
22
|
+
* @module composeUpdate
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Wires `container.update` so widget props and factory props coexist.
|
|
27
|
+
*
|
|
28
|
+
* @param {HTMLElement} container - The widget root returned by `WidgetFactory`.
|
|
29
|
+
* @param {Iterable<string>} widgetPropKeys - Prop names owned by the widget.
|
|
30
|
+
* These are routed to `applyWidgetProps` and never reach the factory.
|
|
31
|
+
* @param {function(Object): void} applyWidgetProps - The widget's own prop
|
|
32
|
+
* application logic. It receives only widget-owned keys and may ignore keys it
|
|
33
|
+
* does not handle (preserving each widget's existing behavior).
|
|
34
|
+
* @returns {HTMLElement} The same container, for chaining.
|
|
35
|
+
*/
|
|
36
|
+
export function composeUpdate(container, widgetPropKeys, applyWidgetProps) {
|
|
37
|
+
const widgetKeys = new Set(widgetPropKeys);
|
|
38
|
+
const factoryUpdate = container.update;
|
|
39
|
+
|
|
40
|
+
// The factory re-processes `_originalProps` on every update; widget-owned keys
|
|
41
|
+
// must not live there or they would be re-applied as styles/attributes from
|
|
42
|
+
// their construction-time values.
|
|
43
|
+
if (container._originalProps) {
|
|
44
|
+
for (const key of widgetKeys) delete container._originalProps[key];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
container.update = (newProps = {}) => {
|
|
48
|
+
const widgetProps = {};
|
|
49
|
+
const factoryProps = {};
|
|
50
|
+
for (const key of Object.keys(newProps)) {
|
|
51
|
+
if (widgetKeys.has(key)) widgetProps[key] = newProps[key];
|
|
52
|
+
else factoryProps[key] = newProps[key];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
applyWidgetProps(widgetProps);
|
|
56
|
+
|
|
57
|
+
if (Object.keys(factoryProps).length > 0) factoryUpdate(factoryProps);
|
|
58
|
+
|
|
59
|
+
return container;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return container;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default composeUpdate;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// utils/getWidgetProps.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gets the current props of a widget
|
|
5
|
+
* @param {HTMLElement} widget - The widget to inspect
|
|
6
|
+
* @returns {Object} Dictionary with the widget's props
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const props = getWidgetProps(myButton);
|
|
10
|
+
* // { text: 'Click', variant: 'filled', color: '#007aff' }
|
|
11
|
+
*/
|
|
12
|
+
export const getWidgetProps = (widget) => {
|
|
13
|
+
return { ...(widget._props || {}) };
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Gets a specific prop from a widget
|
|
18
|
+
* @param {HTMLElement} widget - The widget to inspect
|
|
19
|
+
* @param {string} propName - Name of the prop
|
|
20
|
+
* @returns {any} Prop value or undefined
|
|
21
|
+
*/
|
|
22
|
+
export const getWidgetProp = (widget, propName) => {
|
|
23
|
+
return widget._props ? widget._props[propName] : undefined;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Gets all props as a formatted string
|
|
28
|
+
* @param {HTMLElement} widget - The widget to inspect
|
|
29
|
+
* @returns {string} Formatted props string
|
|
30
|
+
*/
|
|
31
|
+
export const stringifyWidgetProps = (widget) => {
|
|
32
|
+
return JSON.stringify(widget._props || {}, null, 2);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default { getWidgetProps, getWidgetProp, stringifyWidgetProps };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// src/utils/index.js
|
|
2
|
+
/**
|
|
3
|
+
* @module utils
|
|
4
|
+
* @description
|
|
5
|
+
* Central barrel file — re-exports every public utility from the `utils/`
|
|
6
|
+
* directory so consumers can import from a single entry point:
|
|
7
|
+
*
|
|
8
|
+
* ```js
|
|
9
|
+
* import { colors, toREM, addNavigation, TextInputValidator } from './utils';
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* Included utilities:
|
|
13
|
+
* - **themes** — color palette, theme switching, CSS variable sync
|
|
14
|
+
* - **getWidgetProps** — read widget `_props` safely
|
|
15
|
+
* - **syntaxHighlight** — JavaScript/FletBox tokenizer and HTML highlighter
|
|
16
|
+
* - **markdownParser** — markdown → FletBox widget tree converter
|
|
17
|
+
* - **visualEffects** — CSS animations (stripes, shimmer, glow, pulse)
|
|
18
|
+
* - **stackPosition** — dataset-based absolute positioning for Stack children
|
|
19
|
+
* - **units** — px ↔ rem conversion helpers
|
|
20
|
+
* - **navigation** — tree-navigation mixin for widget objects
|
|
21
|
+
* - **stopWebRefresh** — disables browser refresh gestures
|
|
22
|
+
* - **TextInputValidator** — input filtering and XSS sanitization
|
|
23
|
+
* - **mediaTime** — time formatting for media players
|
|
24
|
+
*/
|
|
25
|
+
export {
|
|
26
|
+
colors,
|
|
27
|
+
setTheme,
|
|
28
|
+
getTheme,
|
|
29
|
+
toggleTheme,
|
|
30
|
+
subscribeTheme,
|
|
31
|
+
applySystemTheme,
|
|
32
|
+
watchSystemTheme,
|
|
33
|
+
getColor,
|
|
34
|
+
palettes,
|
|
35
|
+
ThemeProvider,
|
|
36
|
+
useTheme,
|
|
37
|
+
} from "./themes.js";
|
|
38
|
+
export {
|
|
39
|
+
getWidgetProps,
|
|
40
|
+
getWidgetProp,
|
|
41
|
+
stringifyWidgetProps,
|
|
42
|
+
} from "./getWidgetProps.js";
|
|
43
|
+
export {
|
|
44
|
+
tokenize,
|
|
45
|
+
generateHighlightedHtml,
|
|
46
|
+
highlightColors,
|
|
47
|
+
} from "./syntaxHighlight.js";
|
|
48
|
+
export {
|
|
49
|
+
markdownToWidgets,
|
|
50
|
+
parseMarkdownToWidgets,
|
|
51
|
+
parseInlineToWidgets,
|
|
52
|
+
parseMarkdown,
|
|
53
|
+
parseInlineMarkdown,
|
|
54
|
+
escapeHtml,
|
|
55
|
+
} from "./markdownParser.js";
|
|
56
|
+
export {
|
|
57
|
+
applyStripes,
|
|
58
|
+
removeStripes,
|
|
59
|
+
applyShimmer,
|
|
60
|
+
applyGlow,
|
|
61
|
+
applyIndeterminate,
|
|
62
|
+
applyPulse,
|
|
63
|
+
injectKeyframes,
|
|
64
|
+
} from "./visualEffects.js";
|
|
65
|
+
export { default as stackPosition } from "./stackPosition.js";
|
|
66
|
+
export { toREM, setBaseFontSize, toPX, getBaseFontSize } from "./units.js";
|
|
67
|
+
export { addNavigation } from "./navigation.js";
|
|
68
|
+
export { stopWebRefresh } from "./stopWebRefresh.js";
|
|
69
|
+
export { TextInputValidator } from "./TextInputValidator.js";
|
|
70
|
+
export {
|
|
71
|
+
formatMediaTime,
|
|
72
|
+
formatMediaTimeLong,
|
|
73
|
+
getProgressPercent,
|
|
74
|
+
percentToSeconds,
|
|
75
|
+
formatMediaProgress,
|
|
76
|
+
} from "./mediaTime.js";
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// utils/markdownParser.js
|
|
2
|
+
import { DataTable } from "../widgets/DataTable.js";
|
|
3
|
+
import { Text } from "../widgets/Text.js";
|
|
4
|
+
import { Container } from "../widgets/Container.js";
|
|
5
|
+
import { Row } from "../widgets/Row.js";
|
|
6
|
+
import { Icon } from "../widgets/Icon.js";
|
|
7
|
+
import { CodeViewer } from "../widgets/CodeViewer.js";
|
|
8
|
+
import { colors } from "./themes.js";
|
|
9
|
+
|
|
10
|
+
// ========== TABLES ==========
|
|
11
|
+
const isTableLine = (line) => {
|
|
12
|
+
return (
|
|
13
|
+
line.includes("|") &&
|
|
14
|
+
line.trim().startsWith("|") &&
|
|
15
|
+
line.trim().endsWith("|")
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const isTableSeparatorLine = (line) => {
|
|
20
|
+
return (
|
|
21
|
+
line.includes("|") &&
|
|
22
|
+
line.includes("-") &&
|
|
23
|
+
/[\s\|]*:?-+:?[\s\|]*/.test(line)
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const parseTable = (lines, startIndex) => {
|
|
28
|
+
let i = startIndex;
|
|
29
|
+
const headerLine = lines[i].trim();
|
|
30
|
+
const separatorLine = lines[i + 1]?.trim();
|
|
31
|
+
|
|
32
|
+
if (!isTableLine(headerLine)) return null;
|
|
33
|
+
if (!isTableSeparatorLine(separatorLine)) return null;
|
|
34
|
+
|
|
35
|
+
const headers = headerLine
|
|
36
|
+
.split("|")
|
|
37
|
+
.filter((cell) => cell.trim() !== "")
|
|
38
|
+
.map((cell) => cell.trim());
|
|
39
|
+
|
|
40
|
+
if (headers.length === 0) return null;
|
|
41
|
+
|
|
42
|
+
i += 2;
|
|
43
|
+
|
|
44
|
+
const rows = [];
|
|
45
|
+
while (i < lines.length) {
|
|
46
|
+
const line = lines[i].trim();
|
|
47
|
+
if (!isTableLine(line)) break;
|
|
48
|
+
|
|
49
|
+
const cells = line
|
|
50
|
+
.split("|")
|
|
51
|
+
.filter((cell) => cell.trim() !== "")
|
|
52
|
+
.map((cell) => cell.trim());
|
|
53
|
+
|
|
54
|
+
if (cells.length === headers.length) {
|
|
55
|
+
const rowData = {};
|
|
56
|
+
headers.forEach((header, idx) => {
|
|
57
|
+
rowData[header] = cells[idx] || "";
|
|
58
|
+
});
|
|
59
|
+
rows.push(rowData);
|
|
60
|
+
}
|
|
61
|
+
i++;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const columns = headers.map((h) => ({ key: h, label: h }));
|
|
65
|
+
const tableWidget = DataTable({
|
|
66
|
+
columns: columns,
|
|
67
|
+
rows: rows,
|
|
68
|
+
striped: true,
|
|
69
|
+
hoverable: true,
|
|
70
|
+
bordered: true,
|
|
71
|
+
compact: false,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return { widget: tableWidget, nextIndex: i };
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// ========== INLINE TEXT ==========
|
|
78
|
+
/**
|
|
79
|
+
* Parses an inline markdown string and converts it to an array of widgets.
|
|
80
|
+
* Currently handles `**bold**` syntax; all other text is rendered as plain
|
|
81
|
+
* `Text` widgets. Multiple parts are wrapped in a `Row` with word-wrapping.
|
|
82
|
+
* @param {string} text - Inline markdown text to parse.
|
|
83
|
+
* @returns {Array} Array of widget instances (Text or Row).
|
|
84
|
+
*/
|
|
85
|
+
export const parseInlineToWidgets = (text) => {
|
|
86
|
+
if (!text) return [Text({ text: "" })];
|
|
87
|
+
|
|
88
|
+
const parts = [];
|
|
89
|
+
let remaining = text;
|
|
90
|
+
|
|
91
|
+
const boldRegex = /\*\*(.*?)\*\*/;
|
|
92
|
+
let match;
|
|
93
|
+
while ((match = boldRegex.exec(remaining)) !== null) {
|
|
94
|
+
const before = remaining.substring(0, match.index);
|
|
95
|
+
if (before) parts.push(Text({ text: before, size: 14 }));
|
|
96
|
+
parts.push(Text({ text: match[1], size: 14, weight: "bold" }));
|
|
97
|
+
remaining = remaining.substring(match.index + match[0].length);
|
|
98
|
+
}
|
|
99
|
+
if (remaining) parts.push(Text({ text: remaining, size: 14 }));
|
|
100
|
+
|
|
101
|
+
if (parts.length === 0) return [Text({ text: text, size: 14 })];
|
|
102
|
+
if (parts.length === 1) return parts;
|
|
103
|
+
|
|
104
|
+
return [
|
|
105
|
+
Row({ alignItems: "center", gap: 4, children: parts, flexWrap: "wrap" }),
|
|
106
|
+
];
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// ========== MAIN PARSER ==========
|
|
110
|
+
/**
|
|
111
|
+
* Converts a markdown string to an array of FletBox widgets.
|
|
112
|
+
*
|
|
113
|
+
* Supported syntax:
|
|
114
|
+
* - Headings: `#`, `##`, `###`
|
|
115
|
+
* - Bold: `**text**`
|
|
116
|
+
* - Horizontal rules: `---` / `***`
|
|
117
|
+
* - Blockquotes: `> text`
|
|
118
|
+
* - Unordered lists: `- item`, `* item`, `+ item`
|
|
119
|
+
* - Ordered lists: `1. item`
|
|
120
|
+
* - Fenced code blocks: `` ```lang ``
|
|
121
|
+
* - Tables (GFM pipe tables)
|
|
122
|
+
* - Empty lines (spacers)
|
|
123
|
+
* - Plain paragraphs
|
|
124
|
+
*
|
|
125
|
+
* @param {string} text - Markdown source string.
|
|
126
|
+
* @param {Object} [options={}] - Rendering options.
|
|
127
|
+
* @param {number} [options.codeMaxHeight=300] - Maximum height in pixels for code blocks.
|
|
128
|
+
* @param {number} [options.codeFontSize=12] - Font size in pixels for code blocks.
|
|
129
|
+
* @returns {Array} Flat array of widget instances ready to be rendered.
|
|
130
|
+
*/
|
|
131
|
+
export const markdownToWidgets = (text, options = {}) => {
|
|
132
|
+
if (!text) return [];
|
|
133
|
+
|
|
134
|
+
const widgets = [];
|
|
135
|
+
const lines = text.split("\n");
|
|
136
|
+
let i = 0;
|
|
137
|
+
let inCodeBlock = false;
|
|
138
|
+
let codeContent = [];
|
|
139
|
+
let codeLang = "";
|
|
140
|
+
|
|
141
|
+
while (i < lines.length) {
|
|
142
|
+
const line = lines[i];
|
|
143
|
+
|
|
144
|
+
// CODE BLOCKS
|
|
145
|
+
if (!inCodeBlock && line.trim().startsWith("```")) {
|
|
146
|
+
inCodeBlock = true;
|
|
147
|
+
codeLang = line.trim().replace(/```/g, "").trim();
|
|
148
|
+
i++;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (inCodeBlock && line.trim().startsWith("```")) {
|
|
152
|
+
inCodeBlock = false;
|
|
153
|
+
const codeWidget = CodeViewer({
|
|
154
|
+
code: codeContent.join("\n"),
|
|
155
|
+
language: codeLang || "javascript",
|
|
156
|
+
maxHeight: options.codeMaxHeight || 300,
|
|
157
|
+
fontSize: options.codeFontSize || 12,
|
|
158
|
+
showHeader: false,
|
|
159
|
+
showLineNumbers: true,
|
|
160
|
+
});
|
|
161
|
+
widgets.push(codeWidget);
|
|
162
|
+
codeContent = [];
|
|
163
|
+
codeLang = "";
|
|
164
|
+
i++;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (inCodeBlock) {
|
|
168
|
+
codeContent.push(line);
|
|
169
|
+
i++;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// TABLES
|
|
174
|
+
if (
|
|
175
|
+
isTableLine(line) &&
|
|
176
|
+
i + 1 < lines.length &&
|
|
177
|
+
isTableSeparatorLine(lines[i + 1])
|
|
178
|
+
) {
|
|
179
|
+
const result = parseTable(lines, i);
|
|
180
|
+
if (result) {
|
|
181
|
+
widgets.push(result.widget);
|
|
182
|
+
i = result.nextIndex;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// HEADERS
|
|
188
|
+
if (line.match(/^### /)) {
|
|
189
|
+
widgets.push(
|
|
190
|
+
Text({
|
|
191
|
+
text: line.substring(4),
|
|
192
|
+
size: 18,
|
|
193
|
+
weight: "bold",
|
|
194
|
+
marginTop: 16,
|
|
195
|
+
marginBottom: 8,
|
|
196
|
+
}),
|
|
197
|
+
);
|
|
198
|
+
i++;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (line.match(/^## /)) {
|
|
202
|
+
widgets.push(
|
|
203
|
+
Text({
|
|
204
|
+
text: line.substring(3),
|
|
205
|
+
size: 22,
|
|
206
|
+
weight: "bold",
|
|
207
|
+
marginTop: 20,
|
|
208
|
+
marginBottom: 10,
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
i++;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (line.match(/^# /)) {
|
|
215
|
+
widgets.push(
|
|
216
|
+
Text({
|
|
217
|
+
text: line.substring(2),
|
|
218
|
+
size: 28,
|
|
219
|
+
weight: "bold",
|
|
220
|
+
marginTop: 24,
|
|
221
|
+
marginBottom: 12,
|
|
222
|
+
}),
|
|
223
|
+
);
|
|
224
|
+
i++;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// HORIZONTAL RULE
|
|
229
|
+
if (line.match(/^---$/) || line.match(/^\*\*\*$/)) {
|
|
230
|
+
widgets.push(
|
|
231
|
+
Container({ height: 1, bgColor: colors.border, marginVertical: 16 }),
|
|
232
|
+
);
|
|
233
|
+
i++;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// BLOCKQUOTE
|
|
238
|
+
if (line.match(/^> /)) {
|
|
239
|
+
const content = line.substring(2);
|
|
240
|
+
const innerWidgets = parseInlineToWidgets(content);
|
|
241
|
+
widgets.push(
|
|
242
|
+
Container({
|
|
243
|
+
padding: 12,
|
|
244
|
+
bgColor: `${colors.primary}10`,
|
|
245
|
+
borderRadius: 8,
|
|
246
|
+
borderLeft: `4px solid ${colors.primary}`,
|
|
247
|
+
marginVertical: 8,
|
|
248
|
+
child: Row({ alignItems: "center", gap: 8, children: innerWidgets }),
|
|
249
|
+
}),
|
|
250
|
+
);
|
|
251
|
+
i++;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// UNORDERED LIST
|
|
256
|
+
if (line.match(/^\s*[-*+]\s/)) {
|
|
257
|
+
const content = line.replace(/^\s*[-*+]\s/, "");
|
|
258
|
+
const innerWidgets = parseInlineToWidgets(content);
|
|
259
|
+
widgets.push(
|
|
260
|
+
Row({
|
|
261
|
+
alignItems: "center",
|
|
262
|
+
gap: 8,
|
|
263
|
+
marginVertical: 4,
|
|
264
|
+
children: [
|
|
265
|
+
Icon({ name: "circle", size: 8, color: colors.primary }),
|
|
266
|
+
...innerWidgets,
|
|
267
|
+
],
|
|
268
|
+
}),
|
|
269
|
+
);
|
|
270
|
+
i++;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ORDERED LIST
|
|
275
|
+
if (line.match(/^\s*\d+\.\s/)) {
|
|
276
|
+
const number = line.match(/^\s*(\d+)\.\s/)[1];
|
|
277
|
+
const content = line.replace(/^\s*\d+\.\s/, "");
|
|
278
|
+
const innerWidgets = parseInlineToWidgets(content);
|
|
279
|
+
widgets.push(
|
|
280
|
+
Row({
|
|
281
|
+
alignItems: "center",
|
|
282
|
+
gap: 8,
|
|
283
|
+
marginVertical: 4,
|
|
284
|
+
children: [
|
|
285
|
+
Text({
|
|
286
|
+
text: `${number}.`,
|
|
287
|
+
size: 14,
|
|
288
|
+
weight: "bold",
|
|
289
|
+
color: colors.primary,
|
|
290
|
+
}),
|
|
291
|
+
...innerWidgets,
|
|
292
|
+
],
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
295
|
+
i++;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// EMPTY LINE
|
|
300
|
+
if (line.trim() === "") {
|
|
301
|
+
widgets.push(Container({ height: 8 }));
|
|
302
|
+
i++;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// PARAGRAPH
|
|
307
|
+
const paragraphWidgets = parseInlineToWidgets(line);
|
|
308
|
+
if (paragraphWidgets.length === 1) {
|
|
309
|
+
widgets.push(paragraphWidgets[0]);
|
|
310
|
+
} else {
|
|
311
|
+
widgets.push(
|
|
312
|
+
Row({
|
|
313
|
+
alignItems: "center",
|
|
314
|
+
gap: 4,
|
|
315
|
+
children: paragraphWidgets,
|
|
316
|
+
flexWrap: "wrap",
|
|
317
|
+
marginVertical: 4,
|
|
318
|
+
}),
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
i++;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return widgets;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// ========== MAIN EXPORTS ==========
|
|
328
|
+
/** Alias for {@link markdownToWidgets}. */
|
|
329
|
+
export const parseMarkdownToWidgets = markdownToWidgets;
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Identity function — returns the raw markdown string unchanged.
|
|
333
|
+
* Provided for API symmetry where a parse step is expected but plain text is acceptable.
|
|
334
|
+
* @param {string} text - Markdown source.
|
|
335
|
+
* @returns {string} The same string, unmodified.
|
|
336
|
+
*/
|
|
337
|
+
export const parseMarkdown = (text) => text;
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Identity function — returns the raw inline markdown string unchanged.
|
|
341
|
+
* @param {string} text - Inline markdown source.
|
|
342
|
+
* @returns {string} The same string, unmodified.
|
|
343
|
+
*/
|
|
344
|
+
export const parseInlineMarkdown = (text) => text;
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Escapes HTML special characters to prevent injection when inserting into innerHTML.
|
|
348
|
+
* Converts `& < > " '` to their corresponding HTML entities.
|
|
349
|
+
* @param {string} text - Raw text to escape.
|
|
350
|
+
* @returns {string} HTML-safe string.
|
|
351
|
+
*/
|
|
352
|
+
export const escapeHtml = (text) =>
|
|
353
|
+
String(text)
|
|
354
|
+
.replace(/&/g, "&")
|
|
355
|
+
.replace(/</g, "<")
|
|
356
|
+
.replace(/>/g, ">")
|
|
357
|
+
.replace(/"/g, """)
|
|
358
|
+
.replace(/'/g, "'");
|
|
359
|
+
|
|
360
|
+
// Default export
|
|
361
|
+
export default {
|
|
362
|
+
markdownToWidgets,
|
|
363
|
+
parseMarkdownToWidgets,
|
|
364
|
+
parseInlineToWidgets,
|
|
365
|
+
parseMarkdown,
|
|
366
|
+
parseInlineMarkdown,
|
|
367
|
+
escapeHtml,
|
|
368
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// src/utils/mediaTime.js
|
|
2
|
+
// Time formatting utilities for media players (audio/video)
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Formats a duration in seconds as a `MM:SS` string.
|
|
6
|
+
* Returns `"00:00"` for falsy or `NaN` values.
|
|
7
|
+
* @param {number} seconds - Duration in seconds.
|
|
8
|
+
* @returns {string} Zero-padded `"MM:SS"` string (e.g. `"03:07"`).
|
|
9
|
+
* @example
|
|
10
|
+
* formatMediaTime(187) // "03:07"
|
|
11
|
+
* formatMediaTime(0) // "00:00"
|
|
12
|
+
*/
|
|
13
|
+
export const formatMediaTime = (seconds) => {
|
|
14
|
+
if (!seconds || isNaN(seconds)) return "00:00";
|
|
15
|
+
const mins = Math.floor(seconds / 60);
|
|
16
|
+
const secs = Math.floor(seconds % 60);
|
|
17
|
+
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Formats a duration in seconds as a `HH:MM:SS` or `MM:SS` string,
|
|
22
|
+
* including hours only when the value is 3600 seconds or more.
|
|
23
|
+
* Returns `"00:00"` for falsy or `NaN` values.
|
|
24
|
+
* @param {number} seconds - Duration in seconds.
|
|
25
|
+
* @returns {string} Zero-padded `"HH:MM:SS"` or `"MM:SS"` string.
|
|
26
|
+
* @example
|
|
27
|
+
* formatMediaTimeLong(3723) // "01:02:03"
|
|
28
|
+
* formatMediaTimeLong(187) // "03:07"
|
|
29
|
+
*/
|
|
30
|
+
export const formatMediaTimeLong = (seconds) => {
|
|
31
|
+
if (!seconds || isNaN(seconds)) return "00:00";
|
|
32
|
+
const hrs = Math.floor(seconds / 3600);
|
|
33
|
+
const mins = Math.floor((seconds % 3600) / 60);
|
|
34
|
+
const secs = Math.floor(seconds % 60);
|
|
35
|
+
if (hrs > 0) {
|
|
36
|
+
return `${hrs.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
|
37
|
+
}
|
|
38
|
+
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Calculates the playback progress as a percentage between 0 and 100.
|
|
43
|
+
* Returns `0` when `total` is falsy or zero to avoid division by zero.
|
|
44
|
+
* @param {number} current - Current playback position in seconds.
|
|
45
|
+
* @param {number} total - Total media duration in seconds.
|
|
46
|
+
* @returns {number} Progress percentage clamped to `[0, 100]`.
|
|
47
|
+
* @example
|
|
48
|
+
* getProgressPercent(30, 120) // 25
|
|
49
|
+
* getProgressPercent(0, 0) // 0
|
|
50
|
+
*/
|
|
51
|
+
export const getProgressPercent = (current, total) => {
|
|
52
|
+
if (!total || total <= 0) return 0;
|
|
53
|
+
return Math.min(100, Math.max(0, (current / total) * 100));
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Converts a progress percentage back to an absolute position in seconds.
|
|
58
|
+
* Returns `0` when `total` is falsy or zero.
|
|
59
|
+
* @param {number} percent - Progress value between 0 and 100.
|
|
60
|
+
* @param {number} total - Total media duration in seconds.
|
|
61
|
+
* @returns {number} Corresponding position in seconds.
|
|
62
|
+
* @example
|
|
63
|
+
* percentToSeconds(25, 120) // 30
|
|
64
|
+
*/
|
|
65
|
+
export const percentToSeconds = (percent, total) => {
|
|
66
|
+
if (!total || total <= 0) return 0;
|
|
67
|
+
return (percent / 100) * total;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Formats the combined current/total progress as a `"MM:SS / MM:SS"` string.
|
|
72
|
+
* Delegates to {@link formatMediaTime} for each value.
|
|
73
|
+
* @param {number} current - Current playback position in seconds.
|
|
74
|
+
* @param {number} total - Total media duration in seconds.
|
|
75
|
+
* @returns {string} Progress string (e.g. `"01:23 / 04:56"`).
|
|
76
|
+
*/
|
|
77
|
+
export const formatMediaProgress = (current, total) => {
|
|
78
|
+
return `${formatMediaTime(current)} / ${formatMediaTime(total)}`;
|
|
79
|
+
};
|