@hogoshi/ui 0.0.0-stage → 0.1.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 +63 -4
- package/src/components/avatar.tsx +30 -0
- package/src/components/badge.tsx +26 -0
- package/src/components/breadcrumb.tsx +37 -0
- package/src/components/button.tsx +58 -0
- package/src/components/calendar.tsx +244 -0
- package/src/components/card.tsx +18 -0
- package/src/components/chart.tsx +387 -0
- package/src/components/checkbox.tsx +26 -0
- package/src/components/code-block.tsx +118 -0
- package/src/components/collapsible.tsx +31 -0
- package/src/components/command.tsx +111 -0
- package/src/components/context-menu.tsx +150 -0
- package/src/components/counter.tsx +56 -0
- package/src/components/dialog.tsx +36 -0
- package/src/components/dropdown-menu.tsx +54 -0
- package/src/components/file-upload.tsx +381 -0
- package/src/components/glide.tsx +82 -0
- package/src/components/icon.tsx +28 -0
- package/src/components/image-viewer.tsx +370 -0
- package/src/components/input.tsx +187 -0
- package/src/components/kbd.tsx +14 -0
- package/src/components/logo-mark.ts +8 -0
- package/src/components/logo.tsx +36 -0
- package/src/components/navbar.tsx +144 -0
- package/src/components/otp.tsx +93 -0
- package/src/components/page-loader.tsx +37 -0
- package/src/components/pophover.tsx +28 -0
- package/src/components/popover.tsx +36 -0
- package/src/components/popup.tsx +75 -0
- package/src/components/progress.tsx +26 -0
- package/src/components/result.tsx +22 -0
- package/src/components/scroll-area.tsx +38 -0
- package/src/components/select.tsx +52 -0
- package/src/components/separator.tsx +14 -0
- package/src/components/sheet.tsx +96 -0
- package/src/components/sidebar.tsx +195 -0
- package/src/components/skeleton.tsx +14 -0
- package/src/components/slider.tsx +31 -0
- package/src/components/smooth-scroll.tsx +174 -0
- package/src/components/spinner.tsx +18 -0
- package/src/components/switch.tsx +22 -0
- package/src/components/table.tsx +52 -0
- package/src/components/tabs.tsx +48 -0
- package/src/components/theme-toggle.tsx +214 -0
- package/src/components/toaster.tsx +38 -0
- package/src/components/tooltip.tsx +43 -0
- package/src/components/video-player.tsx +559 -0
- package/src/components/video-settings.tsx +233 -0
- package/src/index.ts +102 -0
- package/src/lib/cn.ts +6 -0
- package/src/lib/jsx-highlight.ts +128 -0
- package/src/styles.css +443 -0
- package/src/theme/theme-script.ts +13 -0
- package/src/theme/theme.tsx +77 -0
- package/styles.css +1 -0
- package/README.md +0 -3
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AlarmClockIcon,
|
|
5
|
+
ArrowLeft01Icon,
|
|
6
|
+
ArrowRight01Icon,
|
|
7
|
+
AspectRatioIcon,
|
|
8
|
+
DashboardSpeed01Icon,
|
|
9
|
+
HdIcon,
|
|
10
|
+
KeyboardIcon,
|
|
11
|
+
RepeatIcon,
|
|
12
|
+
Tick02Icon,
|
|
13
|
+
} from "@hugeicons-pro/core-stroke-rounded";
|
|
14
|
+
import { useLayoutEffect, useRef, useState, type ReactNode } from "react";
|
|
15
|
+
import { cn } from "../lib/cn";
|
|
16
|
+
import { Icon, type AppIcon } from "./icon";
|
|
17
|
+
import { Kbd } from "./kbd";
|
|
18
|
+
import { Switch } from "./switch";
|
|
19
|
+
|
|
20
|
+
export type VideoFit = "contain" | "cover";
|
|
21
|
+
export type SleepTimer = number | "end" | null;
|
|
22
|
+
|
|
23
|
+
export const SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
|
|
24
|
+
|
|
25
|
+
const SLEEP: { value: SleepTimer; label: string }[] = [
|
|
26
|
+
{ value: null, label: "Off" },
|
|
27
|
+
{ value: 15, label: "15 minutes" },
|
|
28
|
+
{ value: 30, label: "30 minutes" },
|
|
29
|
+
{ value: 60, label: "1 hour" },
|
|
30
|
+
{ value: "end", label: "End of video" },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const FITS: { value: VideoFit; label: string }[] = [
|
|
34
|
+
{ value: "contain", label: "Fit" },
|
|
35
|
+
{ value: "cover", label: "Fill" },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const KEYS: [string, string[]][] = [
|
|
39
|
+
["Play or pause", ["Space", "K"]],
|
|
40
|
+
["Back or forward 5s", ["←", "→"]],
|
|
41
|
+
["Back or forward 10s", ["J", "L"]],
|
|
42
|
+
["Volume", ["↑", "↓"]],
|
|
43
|
+
["Mute", ["M"]],
|
|
44
|
+
["Fullscreen", ["F"]],
|
|
45
|
+
["Mini player", ["I"]],
|
|
46
|
+
["Jump to 0–90%", ["0", "9"]],
|
|
47
|
+
["Previous or next frame", [",", "."]],
|
|
48
|
+
["Speed", ["<", ">"]],
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
type View = "main" | "speed" | "quality" | "sleep" | "fit" | "keys";
|
|
52
|
+
|
|
53
|
+
const rowClass =
|
|
54
|
+
"hover:bg-fg/10 flex h-9 w-full cursor-pointer items-center gap-3 rounded-[var(--radius-sm)] px-2.5 text-left text-sm outline-none transition-colors duration-[var(--motion-duration)] focus-visible:bg-fg/10";
|
|
55
|
+
|
|
56
|
+
const speedLabel = (rate: number) => (rate === 1 ? "Normal" : `${rate}×`);
|
|
57
|
+
|
|
58
|
+
export interface VideoSettingsProps {
|
|
59
|
+
rate: number;
|
|
60
|
+
onRate: (rate: number) => void;
|
|
61
|
+
qualities: string[];
|
|
62
|
+
quality: string;
|
|
63
|
+
onQuality: (quality: string) => void;
|
|
64
|
+
sleep: SleepTimer;
|
|
65
|
+
onSleep: (sleep: SleepTimer) => void;
|
|
66
|
+
fit: VideoFit;
|
|
67
|
+
onFit: (fit: VideoFit) => void;
|
|
68
|
+
loop: boolean;
|
|
69
|
+
onLoop: (loop: boolean) => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function Row({ icon, label, value, onClick }: { icon: AppIcon; label: string; value: string; onClick: () => void }) {
|
|
73
|
+
return (
|
|
74
|
+
<button type="button" className={rowClass} onClick={onClick}>
|
|
75
|
+
<Icon icon={icon} variant="stroke" size={18} className="text-fg-muted shrink-0" />
|
|
76
|
+
<span className="flex-1">{label}</span>
|
|
77
|
+
<span className="text-fg-muted text-xs">{value}</span>
|
|
78
|
+
<Icon icon={ArrowRight01Icon} variant="stroke" size={16} className="text-fg-subtle shrink-0" />
|
|
79
|
+
</button>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function Header({ title, onBack }: { title: string; onBack: () => void }) {
|
|
84
|
+
return (
|
|
85
|
+
<button type="button" className={cn(rowClass, "border-border mb-1 rounded-b-none border-b font-medium")} onClick={onBack}>
|
|
86
|
+
<Icon icon={ArrowLeft01Icon} variant="stroke" size={18} className="shrink-0" />
|
|
87
|
+
{title}
|
|
88
|
+
</button>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function Options<T>({
|
|
93
|
+
options,
|
|
94
|
+
value,
|
|
95
|
+
onPick,
|
|
96
|
+
}: {
|
|
97
|
+
options: { value: T; label: string }[];
|
|
98
|
+
value: T;
|
|
99
|
+
onPick: (value: T) => void;
|
|
100
|
+
}) {
|
|
101
|
+
return options.map((option) => (
|
|
102
|
+
<button
|
|
103
|
+
key={option.label}
|
|
104
|
+
type="button"
|
|
105
|
+
role="menuitemradio"
|
|
106
|
+
aria-checked={option.value === value}
|
|
107
|
+
className={rowClass}
|
|
108
|
+
onClick={() => onPick(option.value)}
|
|
109
|
+
>
|
|
110
|
+
<span className="flex size-[18px] shrink-0 items-center justify-center">
|
|
111
|
+
{option.value === value ? <Icon icon={Tick02Icon} variant="stroke" size={16} /> : null}
|
|
112
|
+
</span>
|
|
113
|
+
{option.label}
|
|
114
|
+
</button>
|
|
115
|
+
));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function VideoSettings(props: VideoSettingsProps) {
|
|
119
|
+
const [view, setView] = useState<View>("main");
|
|
120
|
+
const [motion, setMotion] = useState<"forward" | "back" | null>(null);
|
|
121
|
+
const [height, setHeight] = useState<number>();
|
|
122
|
+
const inner = useRef<HTMLDivElement>(null);
|
|
123
|
+
|
|
124
|
+
useLayoutEffect(() => {
|
|
125
|
+
const node = inner.current;
|
|
126
|
+
if (!node) return;
|
|
127
|
+
const observer = new ResizeObserver(() => setHeight(node.offsetHeight));
|
|
128
|
+
observer.observe(node);
|
|
129
|
+
return () => observer.disconnect();
|
|
130
|
+
}, [view]);
|
|
131
|
+
|
|
132
|
+
const go = (next: View) => {
|
|
133
|
+
setMotion(next === "main" ? "back" : "forward");
|
|
134
|
+
setView(next);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const pick =
|
|
138
|
+
<T,>(apply: (value: T) => void) =>
|
|
139
|
+
(value: T) => {
|
|
140
|
+
apply(value);
|
|
141
|
+
go("main");
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const sleepLabel = SLEEP.find((option) => option.value === props.sleep)?.label ?? "Off";
|
|
145
|
+
const fitLabel = FITS.find((option) => option.value === props.fit)?.label ?? "Fit";
|
|
146
|
+
|
|
147
|
+
const views: Record<View, ReactNode> = {
|
|
148
|
+
main: (
|
|
149
|
+
<>
|
|
150
|
+
<Row icon={DashboardSpeed01Icon} label="Speed" value={speedLabel(props.rate)} onClick={() => go("speed")} />
|
|
151
|
+
{props.qualities.length > 1 ? (
|
|
152
|
+
<Row icon={HdIcon} label="Quality" value={props.quality} onClick={() => go("quality")} />
|
|
153
|
+
) : null}
|
|
154
|
+
<Row icon={AlarmClockIcon} label="Sleep timer" value={sleepLabel} onClick={() => go("sleep")} />
|
|
155
|
+
<Row icon={AspectRatioIcon} label="Video fit" value={fitLabel} onClick={() => go("fit")} />
|
|
156
|
+
<label className={rowClass}>
|
|
157
|
+
<Icon icon={RepeatIcon} variant="stroke" size={18} className="text-fg-muted shrink-0" />
|
|
158
|
+
<span className="flex-1">Loop</span>
|
|
159
|
+
<Switch checked={props.loop} onCheckedChange={props.onLoop} />
|
|
160
|
+
</label>
|
|
161
|
+
<Row icon={KeyboardIcon} label="Hotkeys" value="" onClick={() => go("keys")} />
|
|
162
|
+
</>
|
|
163
|
+
),
|
|
164
|
+
speed: (
|
|
165
|
+
<>
|
|
166
|
+
<Header title="Speed" onBack={() => go("main")} />
|
|
167
|
+
<Options
|
|
168
|
+
options={SPEEDS.map((rate) => ({ value: rate, label: speedLabel(rate) }))}
|
|
169
|
+
value={props.rate}
|
|
170
|
+
onPick={pick(props.onRate)}
|
|
171
|
+
/>
|
|
172
|
+
</>
|
|
173
|
+
),
|
|
174
|
+
quality: (
|
|
175
|
+
<>
|
|
176
|
+
<Header title="Quality" onBack={() => go("main")} />
|
|
177
|
+
<Options
|
|
178
|
+
options={props.qualities.map((quality) => ({ value: quality, label: quality }))}
|
|
179
|
+
value={props.quality}
|
|
180
|
+
onPick={pick(props.onQuality)}
|
|
181
|
+
/>
|
|
182
|
+
</>
|
|
183
|
+
),
|
|
184
|
+
sleep: (
|
|
185
|
+
<>
|
|
186
|
+
<Header title="Sleep timer" onBack={() => go("main")} />
|
|
187
|
+
<Options options={SLEEP} value={props.sleep} onPick={pick(props.onSleep)} />
|
|
188
|
+
</>
|
|
189
|
+
),
|
|
190
|
+
fit: (
|
|
191
|
+
<>
|
|
192
|
+
<Header title="Video fit" onBack={() => go("main")} />
|
|
193
|
+
<Options options={FITS} value={props.fit} onPick={pick(props.onFit)} />
|
|
194
|
+
</>
|
|
195
|
+
),
|
|
196
|
+
keys: (
|
|
197
|
+
<>
|
|
198
|
+
<Header title="Hotkeys" onBack={() => go("main")} />
|
|
199
|
+
<div className="grid gap-1.5 px-2.5 pt-1 pb-2">
|
|
200
|
+
{KEYS.map(([label, keys]) => (
|
|
201
|
+
<div key={label} className="flex items-center justify-between gap-3 text-xs">
|
|
202
|
+
<span className="text-fg-muted">{label}</span>
|
|
203
|
+
<span className="flex gap-1">
|
|
204
|
+
{keys.map((key) => (
|
|
205
|
+
<Kbd key={key}>{key}</Kbd>
|
|
206
|
+
))}
|
|
207
|
+
</span>
|
|
208
|
+
</div>
|
|
209
|
+
))}
|
|
210
|
+
</div>
|
|
211
|
+
</>
|
|
212
|
+
),
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<div
|
|
217
|
+
className="overflow-hidden transition-[height] duration-[var(--motion-duration)] ease-[var(--motion-ease)]"
|
|
218
|
+
style={{ height }}
|
|
219
|
+
>
|
|
220
|
+
<div
|
|
221
|
+
ref={inner}
|
|
222
|
+
key={view}
|
|
223
|
+
className={cn(
|
|
224
|
+
"flex flex-col",
|
|
225
|
+
motion === "forward" && "animate-[panel-from-right_var(--motion-duration)_var(--motion-ease)]",
|
|
226
|
+
motion === "back" && "animate-[panel-from-left_var(--motion-duration)_var(--motion-ease)]",
|
|
227
|
+
)}
|
|
228
|
+
>
|
|
229
|
+
{views[view]}
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export { Avatar, AvatarFallback, AvatarImage } from "./components/avatar";
|
|
2
|
+
export { Badge } from "./components/badge";
|
|
3
|
+
export type { BadgeProps } from "./components/badge";
|
|
4
|
+
export { Button, buttonVariants } from "./components/button";
|
|
5
|
+
export type { ButtonProps } from "./components/button";
|
|
6
|
+
export {
|
|
7
|
+
BottomBar,
|
|
8
|
+
BottomBarItem,
|
|
9
|
+
Sidebar,
|
|
10
|
+
SidebarFooter,
|
|
11
|
+
SidebarGroup,
|
|
12
|
+
SidebarHeader,
|
|
13
|
+
SidebarInset,
|
|
14
|
+
SidebarItem,
|
|
15
|
+
SidebarLabel,
|
|
16
|
+
SidebarNav,
|
|
17
|
+
SidebarProvider,
|
|
18
|
+
SidebarTrigger,
|
|
19
|
+
sidebarLabel,
|
|
20
|
+
useSidebar,
|
|
21
|
+
} from "./components/sidebar";
|
|
22
|
+
export { Card, CardDescription, CardHeader, CardTitle } from "./components/card";
|
|
23
|
+
export { BootScreen, PageLoader } from "./components/page-loader";
|
|
24
|
+
export { Breadcrumb } from "./components/breadcrumb";
|
|
25
|
+
export type { BreadcrumbItem } from "./components/breadcrumb";
|
|
26
|
+
export { Result } from "./components/result";
|
|
27
|
+
export { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./components/table";
|
|
28
|
+
export type { SortDirection } from "./components/table";
|
|
29
|
+
export { Progress } from "./components/progress";
|
|
30
|
+
export { Logo } from "./components/logo";
|
|
31
|
+
export { LOGO_PATH, LOGO_VIEWBOX, logoSvg } from "./components/logo-mark";
|
|
32
|
+
export { Navbar, NavbarItem, NavbarMenu, NavbarMenuLink, NavbarMenus } from "./components/navbar";
|
|
33
|
+
export { Glide, glideActive } from "./components/glide";
|
|
34
|
+
export type { GlideProps } from "./components/glide";
|
|
35
|
+
export { Calendar } from "./components/calendar";
|
|
36
|
+
export { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "./components/collapsible";
|
|
37
|
+
export type { CalendarRange } from "./components/calendar";
|
|
38
|
+
export { CodeBlock } from "./components/code-block";
|
|
39
|
+
export { Counter } from "./components/counter";
|
|
40
|
+
export { AreaChart, BarChart, ChartContainer, ChartLegend, DonutChart, LineChart, Sparkline } from "./components/chart";
|
|
41
|
+
export type { CartesianChartProps, ChartConfig } from "./components/chart";
|
|
42
|
+
export { Checkbox } from "./components/checkbox";
|
|
43
|
+
export {
|
|
44
|
+
Command,
|
|
45
|
+
CommandDialog,
|
|
46
|
+
CommandEmpty,
|
|
47
|
+
CommandGroup,
|
|
48
|
+
CommandInput,
|
|
49
|
+
CommandItem,
|
|
50
|
+
CommandList,
|
|
51
|
+
CommandShortcut,
|
|
52
|
+
} from "./components/command";
|
|
53
|
+
export {
|
|
54
|
+
ContextMenu,
|
|
55
|
+
ContextMenuContent,
|
|
56
|
+
ContextMenuItem,
|
|
57
|
+
ContextMenuRoot,
|
|
58
|
+
ContextMenuTrigger,
|
|
59
|
+
ContextMenuViewport,
|
|
60
|
+
} from "./components/context-menu";
|
|
61
|
+
export type { ContextMenuEntry } from "./components/context-menu";
|
|
62
|
+
export { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle, DialogTrigger } from "./components/dialog";
|
|
63
|
+
export {
|
|
64
|
+
DropdownMenu,
|
|
65
|
+
DropdownMenuContent,
|
|
66
|
+
DropdownMenuItem,
|
|
67
|
+
DropdownMenuSub,
|
|
68
|
+
DropdownMenuSubContent,
|
|
69
|
+
DropdownMenuSubTrigger,
|
|
70
|
+
DropdownMenuTrigger,
|
|
71
|
+
} from "./components/dropdown-menu";
|
|
72
|
+
export { FileList, FileUpload, FileUploadDropzone, formatBytes } from "./components/file-upload";
|
|
73
|
+
export type { FileRejection, FileUploadProps } from "./components/file-upload";
|
|
74
|
+
export { Icon } from "./components/icon";
|
|
75
|
+
export { Image, ImageGallery, ImageViewer, downloadImage } from "./components/image-viewer";
|
|
76
|
+
export type { ViewerImage } from "./components/image-viewer";
|
|
77
|
+
export { Input, Label, Textarea } from "./components/input";
|
|
78
|
+
export { Kbd } from "./components/kbd";
|
|
79
|
+
export type { AppIcon, IconProps, IconVariant } from "./components/icon";
|
|
80
|
+
export { Otp } from "./components/otp";
|
|
81
|
+
export { Popover, PopoverContent, PopoverTrigger } from "./components/popover";
|
|
82
|
+
export { Pophover, PophoverContent, PophoverTrigger } from "./components/pophover";
|
|
83
|
+
export { ScrollArea } from "./components/scroll-area";
|
|
84
|
+
export { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./components/select";
|
|
85
|
+
export { Separator } from "./components/separator";
|
|
86
|
+
export { Sheet, SheetClose, SheetContent, SheetDescription, SheetTitle, SheetTrigger } from "./components/sheet";
|
|
87
|
+
export type { SheetSide } from "./components/sheet";
|
|
88
|
+
export { Slider } from "./components/slider";
|
|
89
|
+
export { Spinner } from "./components/spinner";
|
|
90
|
+
export { Skeleton } from "./components/skeleton";
|
|
91
|
+
export { Switch } from "./components/switch";
|
|
92
|
+
export { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/tabs";
|
|
93
|
+
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./components/tooltip";
|
|
94
|
+
export { SmoothScroll, scrollToTop } from "./components/smooth-scroll";
|
|
95
|
+
export { Toaster, toast } from "./components/toaster";
|
|
96
|
+
export { ThemeToggle } from "./components/theme-toggle";
|
|
97
|
+
export { VideoPlayer, formatTime } from "./components/video-player";
|
|
98
|
+
export type { VideoPlayerProps, VideoSource } from "./components/video-player";
|
|
99
|
+
export type { SleepTimer, VideoFit } from "./components/video-settings";
|
|
100
|
+
export { ThemeProvider, useTheme } from "./theme/theme";
|
|
101
|
+
export type { ColorMode, ResolvedMode } from "./theme/theme";
|
|
102
|
+
export { THEME_INIT_SCRIPT } from "./theme/theme-script";
|
package/src/lib/cn.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
function escapeHtml(value: string) {
|
|
2
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function span(className: string, text: string) {
|
|
6
|
+
return `<span class="${className}">${escapeHtml(text)}</span>`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isTagStart(source: string, index: number) {
|
|
10
|
+
if (source[index] !== "<") return false;
|
|
11
|
+
const next = source[index + 1];
|
|
12
|
+
if (next === "/") return /[A-Za-z]/.test(source[index + 2] ?? "");
|
|
13
|
+
return /[A-Za-z]/.test(next ?? "");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function highlightJsx(source: string) {
|
|
17
|
+
let index = 0;
|
|
18
|
+
let html = "";
|
|
19
|
+
|
|
20
|
+
const readString = (quote: string) => {
|
|
21
|
+
let end = index + 1;
|
|
22
|
+
while (end < source.length) {
|
|
23
|
+
if (source[end] === "\\") {
|
|
24
|
+
end += 2;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (source[end] === quote) {
|
|
28
|
+
end += 1;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
end += 1;
|
|
32
|
+
}
|
|
33
|
+
const text = source.slice(index, end);
|
|
34
|
+
index = end;
|
|
35
|
+
return span("hljs-string", text);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const readTag = () => {
|
|
39
|
+
let chunk = "";
|
|
40
|
+
const closing = source[index + 1] === "/";
|
|
41
|
+
chunk += span("hljs-punctuation", closing ? "</" : "<");
|
|
42
|
+
index += closing ? 2 : 1;
|
|
43
|
+
const nameStart = index;
|
|
44
|
+
while (index < source.length && /[A-Za-z0-9_$.]/.test(source[index])) index += 1;
|
|
45
|
+
chunk += span("hljs-name", source.slice(nameStart, index));
|
|
46
|
+
if (closing) {
|
|
47
|
+
while (index < source.length && source[index] !== ">") {
|
|
48
|
+
chunk += span("hljs-punctuation", source[index]);
|
|
49
|
+
index += 1;
|
|
50
|
+
}
|
|
51
|
+
if (source[index] === ">") {
|
|
52
|
+
chunk += span("hljs-punctuation", ">");
|
|
53
|
+
index += 1;
|
|
54
|
+
}
|
|
55
|
+
return chunk;
|
|
56
|
+
}
|
|
57
|
+
while (index < source.length) {
|
|
58
|
+
const char = source[index];
|
|
59
|
+
if (char === "/" && source[index + 1] === ">") {
|
|
60
|
+
chunk += span("hljs-punctuation", "/>");
|
|
61
|
+
index += 2;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
if (char === ">") {
|
|
65
|
+
chunk += span("hljs-punctuation", ">");
|
|
66
|
+
index += 1;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
if (char === " " || char === "\n" || char === "\t" || char === "\r") {
|
|
70
|
+
chunk += char;
|
|
71
|
+
index += 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (char === '"' || char === "'") {
|
|
75
|
+
chunk += readString(char);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (char === "=" || char === "{" || char === "}" || char === "?" || char === "|" || char === "(" || char === ")" || char === ",") {
|
|
79
|
+
chunk += span("hljs-punctuation", char);
|
|
80
|
+
index += 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (/[0-9]/.test(char)) {
|
|
84
|
+
const start = index;
|
|
85
|
+
while (index < source.length && /[0-9.]/.test(source[index])) index += 1;
|
|
86
|
+
chunk += span("hljs-number", source.slice(start, index));
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (/[A-Za-z_$]/.test(char)) {
|
|
90
|
+
const start = index;
|
|
91
|
+
while (index < source.length && /[A-Za-z0-9_$.]/.test(source[index])) index += 1;
|
|
92
|
+
const word = source.slice(start, index);
|
|
93
|
+
let look = index;
|
|
94
|
+
while (look < source.length && (source[look] === " " || source[look] === "\n" || source[look] === "\t")) look += 1;
|
|
95
|
+
chunk += span(source[look] === "=" ? "hljs-attr" : "hljs-boolean", word);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
chunk += span("hljs-punctuation", char);
|
|
99
|
+
index += 1;
|
|
100
|
+
}
|
|
101
|
+
return chunk;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
while (index < source.length) {
|
|
105
|
+
if (isTagStart(source, index)) {
|
|
106
|
+
html += readTag();
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (source[index] === '"' || source[index] === "'" || source[index] === "`") {
|
|
110
|
+
html += readString(source[index]);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
let end = index + 1;
|
|
114
|
+
while (
|
|
115
|
+
end < source.length &&
|
|
116
|
+
!isTagStart(source, end) &&
|
|
117
|
+
source[end] !== '"' &&
|
|
118
|
+
source[end] !== "'" &&
|
|
119
|
+
source[end] !== "`"
|
|
120
|
+
) {
|
|
121
|
+
end += 1;
|
|
122
|
+
}
|
|
123
|
+
html += escapeHtml(source.slice(index, end));
|
|
124
|
+
index = end;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return html;
|
|
128
|
+
}
|