@myelophone/nuxt 0.18.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/.devcontainer/Dockerfile +7 -0
- package/.devcontainer/devcontainer.json +21 -0
- package/.editorconfig +43 -0
- package/.github/workflows/package.yml +73 -0
- package/.nuxtignore +6 -0
- package/.releaserc.js +132 -0
- package/.yarn/releases/yarn-4.18.0.cjs +1000 -0
- package/.yarnrc.yml +8 -0
- package/CHANGELOG.md +141 -0
- package/LICENSE +77 -0
- package/README.md +3004 -0
- package/app/app.vue +455 -0
- package/app/assets/css/animation.css +81 -0
- package/app/assets/css/backgrounds.css +57 -0
- package/app/assets/css/base.css +182 -0
- package/app/assets/css/index.css +9 -0
- package/app/assets/css/links.css +12 -0
- package/app/assets/css/multimedia.css +102 -0
- package/app/assets/css/preloader.css +68 -0
- package/app/assets/css/print.css +40 -0
- package/app/assets/css/tailwind.css +20 -0
- package/app/assets/css/theme.css +29 -0
- package/app/components/consent/GoogleMap.vue +59 -0
- package/app/components/consent/Youtube.vue +27 -0
- package/app/components/cookie/Banner.vue +70 -0
- package/app/components/cookie/ConsentWrapper.vue +71 -0
- package/app/components/cookie/PrivacyPolicy.vue +179 -0
- package/app/components/cookie/SettingsModal.vue +387 -0
- package/app/components/grid/Col.vue +262 -0
- package/app/components/grid/Container.vue +236 -0
- package/app/components/grid/Row.vue +248 -0
- package/app/components/grid/Stack.vue +233 -0
- package/app/components/myelophone/Copyright.vue +5 -0
- package/app/components/myelophone/Welcome.vue +107 -0
- package/app/components/page/FullscreenPreloader.vue +177 -0
- package/app/components/page/Preloader.vue +18 -0
- package/app/components/page/ReadyLocalized.vue +19 -0
- package/app/components/ready/.gitkeep +0 -0
- package/app/components/safe/Email.vue +52 -0
- package/app/components/safe/ImgWithLoader.vue +180 -0
- package/app/components/safe/NuxtImg.vue +25 -0
- package/app/components/safe/NuxtPicture.vue +29 -0
- package/app/components/section/.gitkeep +0 -0
- package/app/components/seo/BrandInfo.vue +76 -0
- package/app/components/seo/ContentNoIndex.vue +35 -0
- package/app/components/seo/NoIndex.vue +22 -0
- package/app/components/seo/OrgInfo.vue +130 -0
- package/app/components/seo/ProfileInfo.vue +137 -0
- package/app/components/ui/Accordion.vue +163 -0
- package/app/components/ui/Alert.vue +179 -0
- package/app/components/ui/Avatar.vue +88 -0
- package/app/components/ui/Badge.vue +150 -0
- package/app/components/ui/Banner.vue +133 -0
- package/app/components/ui/Button.vue +175 -0
- package/app/components/ui/Card.vue +66 -0
- package/app/components/ui/Checkbox.vue +134 -0
- package/app/components/ui/Chip.vue +136 -0
- package/app/components/ui/CommandPalette.vue +434 -0
- package/app/components/ui/Console.vue +583 -0
- package/app/components/ui/CursorCreative.vue +148 -0
- package/app/components/ui/FeatureAccordion.vue +165 -0
- package/app/components/ui/FileUpload.vue +705 -0
- package/app/components/ui/GeoDependent.vue +72 -0
- package/app/components/ui/Heading.vue +51 -0
- package/app/components/ui/Icon.vue +42 -0
- package/app/components/ui/Icon8.vue +39 -0
- package/app/components/ui/Input.vue +88 -0
- package/app/components/ui/LangLink.vue +34 -0
- package/app/components/ui/LangVisible.vue +33 -0
- package/app/components/ui/LanguageSelect.vue +202 -0
- package/app/components/ui/LightBox.vue +155 -0
- package/app/components/ui/LocalTime.vue +170 -0
- package/app/components/ui/Modal.vue +161 -0
- package/app/components/ui/Popup.vue +200 -0
- package/app/components/ui/PostContent.server.vue +23 -0
- package/app/components/ui/ProtectedEmail.vue +52 -0
- package/app/components/ui/RelativeTime.vue +141 -0
- package/app/components/ui/ScheduledContent.vue +150 -0
- package/app/components/ui/ScrollToTop.client.vue +48 -0
- package/app/components/ui/SegmentedControl.vue +148 -0
- package/app/components/ui/SiteSearch.vue +1191 -0
- package/app/components/ui/Skeleton.vue +91 -0
- package/app/components/ui/SmartContrast.vue +74 -0
- package/app/components/ui/SnapContainer.vue +222 -0
- package/app/components/ui/SnapSection.vue +294 -0
- package/app/components/ui/SonnerStyle.vue +7 -0
- package/app/components/ui/SplitSection.vue +99 -0
- package/app/components/ui/Steps.vue +858 -0
- package/app/components/ui/StickyWrapper.vue +46 -0
- package/app/components/ui/Table.vue +251 -0
- package/app/components/ui/Tabs.vue +451 -0
- package/app/components/ui/TextColumn.vue +36 -0
- package/app/components/ui/Textarea.vue +251 -0
- package/app/components/ui/Toggler.vue +40 -0
- package/app/components/ui/Tooltip.vue +524 -0
- package/app/components/ui/TruncateText.vue +105 -0
- package/app/components/ui/ViewportSpacer.vue +72 -0
- package/app/components/view/AnimatedCounter.vue +121 -0
- package/app/components/view/AnnouncementBar.vue +104 -0
- package/app/components/view/AvatarGroup.vue +104 -0
- package/app/components/view/Breadcrumbs.vue +97 -0
- package/app/components/view/ContentWithToc.vue +152 -0
- package/app/components/view/Copyright.vue +68 -0
- package/app/components/view/CountdownTimer.vue +184 -0
- package/app/components/view/CreativeCTA.vue +143 -0
- package/app/components/view/CurrencySelect.vue +254 -0
- package/app/components/view/FeatureBoxGrid.vue +142 -0
- package/app/components/view/HorizontalMenu.vue +97 -0
- package/app/components/view/ImageCompare.vue +141 -0
- package/app/components/view/ImgSlider.vue +333 -0
- package/app/components/view/InfiniteMarquee.vue +147 -0
- package/app/components/view/InfoBar.vue +86 -0
- package/app/components/view/LogoGrid.vue +106 -0
- package/app/components/view/LogoSlider.vue +83 -0
- package/app/components/view/NotFound.vue +81 -0
- package/app/components/view/PartialBackground.vue +313 -0
- package/app/components/view/QuoteBig.vue +111 -0
- package/app/components/view/ResponsiveMenu.vue +219 -0
- package/app/components/view/ReviewsSlider.vue +230 -0
- package/app/components/view/SocialBar.vue +78 -0
- package/app/components/view/StickyHeadingContent.vue +158 -0
- package/app/components/view/TallyForm.vue +295 -0
- package/app/components/view/WarningOutdated.vue +113 -0
- package/app/composables/useApi.ts +88 -0
- package/app/composables/useAppSeo.ts +78 -0
- package/app/composables/useBreadcrumbs.ts +106 -0
- package/app/composables/useChangeUrl.ts +73 -0
- package/app/composables/useCommand.ts +28 -0
- package/app/composables/useCookieControl.ts +111 -0
- package/app/composables/useCookieModal.ts +9 -0
- package/app/composables/useDevice.ts +109 -0
- package/app/composables/useGeoFetch.ts +139 -0
- package/app/composables/useLoadScript.ts +36 -0
- package/app/composables/useModalState.ts +37 -0
- package/app/composables/useMultiLang.ts +278 -0
- package/app/composables/useNetwork.ts +49 -0
- package/app/composables/useNuxtVersion.ts +5 -0
- package/app/composables/usePiniaStore.ts +5 -0
- package/app/composables/usePopupControl.ts +57 -0
- package/app/composables/useQuery.ts +307 -0
- package/app/composables/useReadyPage.ts +182 -0
- package/app/composables/useSafeList.ts +90 -0
- package/app/composables/useScriptLoader.ts +221 -0
- package/app/composables/useSiteSearch.ts +32 -0
- package/app/composables/useSnapScroll.ts +24 -0
- package/app/composables/useStatic.ts +5 -0
- package/app/composables/useStorage.ts +252 -0
- package/app/composables/useStoreBroadcast.ts +146 -0
- package/app/composables/useThemeSync.ts +81 -0
- package/app/constants/layout.ts +109 -0
- package/app/constants/predefinedCookieScripts.ts +1222 -0
- package/app/constants/ui-events.ts +1 -0
- package/app/error.vue +169 -0
- package/app/layouts/default-blog.vue +5 -0
- package/app/layouts/default.vue +3 -0
- package/app/locales/en/commands.json +5 -0
- package/app/locales/en/cookies.json +182 -0
- package/app/locales/en/error.json +4 -0
- package/app/locales/en/interface.json +33 -0
- package/app/locales/en/myelophone.json +8 -0
- package/app/locales/en/outdated.json +5 -0
- package/app/locales/en/times.json +18 -0
- package/app/locales/pl/commands.json +5 -0
- package/app/locales/pl/cookies.json +182 -0
- package/app/locales/pl/error.json +4 -0
- package/app/locales/pl/interface.json +33 -0
- package/app/locales/pl/myelophone.json +8 -0
- package/app/locales/pl/outdated.json +5 -0
- package/app/locales/pl/times.json +18 -0
- package/app/locales/ru/commands.json +5 -0
- package/app/locales/ru/cookies.json +182 -0
- package/app/locales/ru/error.json +4 -0
- package/app/locales/ru/interface.json +33 -0
- package/app/locales/ru/myelophone.json +8 -0
- package/app/locales/ru/outdated.json +5 -0
- package/app/locales/ru/times.json +18 -0
- package/app/middleware/blog-layout.ts +28 -0
- package/app/middleware/i18n.global.ts +17 -0
- package/app/middleware/not-found-status.ts +11 -0
- package/app/modules/default-blog/index.ts +39 -0
- package/app/modules/default-blog/posts.ts +80 -0
- package/app/modules/default-sitemap/index.ts +116 -0
- package/app/modules/default-sitemap/runtime/sitemap.get.ts +42 -0
- package/app/modules/fullscreen-preloader/module.ts +43 -0
- package/app/modules/htaccess/.htaccess +38 -0
- package/app/modules/htaccess/index.ts +23 -0
- package/app/modules/i18n-shaker.mjs +221 -0
- package/app/modules/multi18n/module.ts +71 -0
- package/app/modules/myelophone-config/module.ts +241 -0
- package/app/pages/[...all].vue +9 -0
- package/app/pages/index.vue +3 -0
- package/app/pages/post/[...slug].vue +30 -0
- package/app/plugins/bfcache-refresh.client.ts +9 -0
- package/app/plugins/canonical.ts +30 -0
- package/app/plugins/csp-nonce.server.ts +41 -0
- package/app/plugins/multi18n.client.ts +21 -0
- package/app/plugins/multi18n.server.ts +15 -0
- package/app/plugins/nuxt-version.client.ts +4 -0
- package/app/plugins/restore.client.ts +21 -0
- package/app/plugins/reveal.ts +89 -0
- package/app/plugins/url-helper.client.ts +66 -0
- package/app/server/api/exchange-rates.get.ts +216 -0
- package/app/server/api/healthz.ts +1 -0
- package/app/server/api/logout.post.ts +6 -0
- package/app/server/middleware/normalize-url.ts +14 -0
- package/app/server/middleware/security-headers.ts +147 -0
- package/app/server/middleware/url-helper.ts +43 -0
- package/app/server/plugins/clean-html-spaces.ts +15 -0
- package/app/server/plugins/clean-styles.ts +94 -0
- package/app/server/plugins/compression.ts +122 -0
- package/app/server/plugins/css-preload.ts +26 -0
- package/app/server/plugins/early-hints.ts +31 -0
- package/app/server/plugins/force-noindex.ts +43 -0
- package/app/server/plugins/html-one-line.ts +17 -0
- package/app/server/plugins/inline-style.ts +7 -0
- package/app/server/routes/healthz.get.ts +1 -0
- package/app/server/tsconfig.json +3 -0
- package/app/stores/cart.ts +945 -0
- package/app/stores/commands.ts +43 -0
- package/app/stores/device.ts +156 -0
- package/app/stores/querySync.ts +5 -0
- package/app/stores/settings.ts +145 -0
- package/app/stores/user.ts +607 -0
- package/app/types/cookie.ts +89 -0
- package/app/types/myelophone.d.ts +87 -0
- package/app/types/site-search.ts +110 -0
- package/app/utils/asyncRefresh.ts +4 -0
- package/app/utils/blog.server.ts +142 -0
- package/app/utils/jsonLd.ts +56 -0
- package/app/utils/myelophoneConfig.ts +4 -0
- package/app/utils/sortBy.ts +18 -0
- package/app/utils/transliterate.ts +75 -0
- package/app/utils/ytVideo.ts +37 -0
- package/app/workers/site-search.worker.ts +661 -0
- package/biome.json +94 -0
- package/commitlint.config.js +3 -0
- package/nuxt.config.ts +539 -0
- package/package.json +101 -0
- package/playground/content/.gitkeep +0 -0
- package/playground/locales/.gitkeep +0 -0
- package/playground/myelophone.dev.ts +45 -0
- package/playground/nuxt.config.ts +105 -0
- package/playground/package.json +5 -0
- package/playground/pages/.gitkeep +0 -0
- package/playground/tsconfig.json +3 -0
- package/playwright.config.ts +23 -0
- package/public/browsers/chrome.svg +77 -0
- package/public/browsers/edge.svg +98 -0
- package/public/browsers/firefox.svg +300 -0
- package/public/favicon.ico +0 -0
- package/public/myelophone_eng.png +0 -0
- package/public/myelophone_eng_white.png +0 -0
- package/spa-loading-template.html +72 -0
- package/tests/home.spec.ts +9 -0
- package/tests/utils.ts +23 -0
- package/tsconfig.json +16 -0
- package/vite/plugins/clean-css.ts +190 -0
- package/vite/plugins/postcss-add-viewportunits.ts +33 -0
- package/vite/plugins/postcss-viewport-fallback.ts +49 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { CookieScriptsConfig, CookieControlConfig } from '~/types/cookie';
|
|
2
|
+
|
|
3
|
+
export interface MyelophoneMulti18nConfig {
|
|
4
|
+
defaultLocale: string;
|
|
5
|
+
locales: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PageFullscreenPreloaderConfig {
|
|
9
|
+
component?: string;
|
|
10
|
+
props?: Record<string, unknown>;
|
|
11
|
+
transparent?: boolean;
|
|
12
|
+
background?: string;
|
|
13
|
+
backgroundDark?: string;
|
|
14
|
+
zIndex?: number | string;
|
|
15
|
+
ariaLabel?: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
contentClass?: string;
|
|
18
|
+
minimumDuration?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SiteSearchConfig {
|
|
22
|
+
strategy?: string;
|
|
23
|
+
endpoint?: string;
|
|
24
|
+
minQueryLength?: number;
|
|
25
|
+
limit?: number;
|
|
26
|
+
operator?: string;
|
|
27
|
+
queryParam?: string;
|
|
28
|
+
modeParam?: string;
|
|
29
|
+
operatorParam?: string;
|
|
30
|
+
urlMode?: string;
|
|
31
|
+
consumeUrl?: boolean;
|
|
32
|
+
enabled?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface MyelophoneStoresConfig {
|
|
36
|
+
cart: boolean;
|
|
37
|
+
user: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface MyelophoneBlogConfig {
|
|
41
|
+
blogEnabled?: boolean;
|
|
42
|
+
postsLayout?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface MyelophoneTallyConfig {
|
|
46
|
+
domain?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface MyelophoneConfig {
|
|
50
|
+
siteDomain?: string;
|
|
51
|
+
cookieScripts?: CookieScriptsConfig;
|
|
52
|
+
cookieControl?: CookieControlConfig;
|
|
53
|
+
bundleTranslations?: boolean;
|
|
54
|
+
splitCss?: boolean;
|
|
55
|
+
stores: MyelophoneStoresConfig;
|
|
56
|
+
frankfurterCurrencies: string[];
|
|
57
|
+
frankfurterBaseCurrency: string;
|
|
58
|
+
creativeCursor: boolean;
|
|
59
|
+
siteSearch: SiteSearchConfig;
|
|
60
|
+
pageFullscreenPreloader?: PageFullscreenPreloaderConfig;
|
|
61
|
+
tally?: MyelophoneTallyConfig;
|
|
62
|
+
noindex?: boolean;
|
|
63
|
+
blog?: MyelophoneBlogConfig;
|
|
64
|
+
ssrStream?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface MyelophoneClientConfig extends MyelophoneConfig {
|
|
68
|
+
multi18n: MyelophoneMulti18nConfig;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
declare global {
|
|
72
|
+
const __MYELOPHONE_CONFIG__: MyelophoneClientConfig;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
declare module '@nuxt/schema' {
|
|
76
|
+
interface RuntimeConfig {
|
|
77
|
+
myelophone: MyelophoneClientConfig;
|
|
78
|
+
}
|
|
79
|
+
interface NuxtConfig {
|
|
80
|
+
myelophone?: Partial<MyelophoneConfig>;
|
|
81
|
+
}
|
|
82
|
+
interface NuxtOptions {
|
|
83
|
+
myelophone?: MyelophoneConfig;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export interface SiteSearchDocument {
|
|
2
|
+
id: string;
|
|
3
|
+
url: string;
|
|
4
|
+
path: string;
|
|
5
|
+
title: string;
|
|
6
|
+
description: string;
|
|
7
|
+
headings: string;
|
|
8
|
+
content: string;
|
|
9
|
+
locale: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type SiteSearchOperator = 'and' | 'or';
|
|
13
|
+
export type SiteSearchStrategy = 'client' | 'remote';
|
|
14
|
+
export type SiteSearchRange = [number, number];
|
|
15
|
+
|
|
16
|
+
export interface SiteSearchRemoteResult {
|
|
17
|
+
id?: string;
|
|
18
|
+
url?: string;
|
|
19
|
+
path: string;
|
|
20
|
+
title: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
snippet?: string;
|
|
23
|
+
locale?: string;
|
|
24
|
+
score?: number;
|
|
25
|
+
highlights?: {
|
|
26
|
+
title?: SiteSearchRange[];
|
|
27
|
+
snippet?: SiteSearchRange[];
|
|
28
|
+
path?: SiteSearchRange[];
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SiteSearchRemoteResponse {
|
|
33
|
+
results: SiteSearchRemoteResult[];
|
|
34
|
+
total?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SiteSearchResult {
|
|
38
|
+
id: string;
|
|
39
|
+
url: string;
|
|
40
|
+
path: string;
|
|
41
|
+
title: string;
|
|
42
|
+
description: string;
|
|
43
|
+
snippet: string;
|
|
44
|
+
titleMatches: SiteSearchRange[];
|
|
45
|
+
snippetMatches: SiteSearchRange[];
|
|
46
|
+
pathMatches: SiteSearchRange[];
|
|
47
|
+
locale: string;
|
|
48
|
+
score: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SiteSearchWorkerOptions {
|
|
52
|
+
origin: string;
|
|
53
|
+
basePath: string;
|
|
54
|
+
seedUrls: string[];
|
|
55
|
+
locales: string[];
|
|
56
|
+
defaultLocale: string;
|
|
57
|
+
maxPages: number;
|
|
58
|
+
maxContentLength: number;
|
|
59
|
+
concurrency: number;
|
|
60
|
+
cacheTtl: number;
|
|
61
|
+
respectNoIndex: boolean;
|
|
62
|
+
discoverSitemaps: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type SiteSearchWorkerRequest =
|
|
66
|
+
| {
|
|
67
|
+
type: 'init';
|
|
68
|
+
requestId: number;
|
|
69
|
+
options: SiteSearchWorkerOptions;
|
|
70
|
+
}
|
|
71
|
+
| {
|
|
72
|
+
type: 'search';
|
|
73
|
+
requestId: number;
|
|
74
|
+
query: string;
|
|
75
|
+
locale: string;
|
|
76
|
+
allLocales: boolean;
|
|
77
|
+
operator: SiteSearchOperator;
|
|
78
|
+
limit: number;
|
|
79
|
+
}
|
|
80
|
+
| {
|
|
81
|
+
type: 'clear-cache';
|
|
82
|
+
requestId: number;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type SiteSearchWorkerResponse =
|
|
86
|
+
| {
|
|
87
|
+
type: 'progress';
|
|
88
|
+
indexed: number;
|
|
89
|
+
total: number;
|
|
90
|
+
}
|
|
91
|
+
| {
|
|
92
|
+
type: 'ready';
|
|
93
|
+
requestId: number;
|
|
94
|
+
count: number;
|
|
95
|
+
cached: boolean;
|
|
96
|
+
}
|
|
97
|
+
| {
|
|
98
|
+
type: 'results';
|
|
99
|
+
requestId: number;
|
|
100
|
+
results: SiteSearchResult[];
|
|
101
|
+
}
|
|
102
|
+
| {
|
|
103
|
+
type: 'cache-cleared';
|
|
104
|
+
requestId: number;
|
|
105
|
+
}
|
|
106
|
+
| {
|
|
107
|
+
type: 'error';
|
|
108
|
+
requestId: number;
|
|
109
|
+
message: string;
|
|
110
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { parse } from 'yaml';
|
|
2
|
+
import MarkdownIt from 'markdown-it';
|
|
3
|
+
|
|
4
|
+
export interface PostMeta {
|
|
5
|
+
title?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
image?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface Post {
|
|
11
|
+
content: string;
|
|
12
|
+
meta: PostMeta;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type PostLoader = () => Promise<string>;
|
|
16
|
+
|
|
17
|
+
const postCache = new Map<string, Promise<Post>>();
|
|
18
|
+
const markdown = new MarkdownIt({
|
|
19
|
+
html: false,
|
|
20
|
+
linkify: true,
|
|
21
|
+
typographer: true,
|
|
22
|
+
});
|
|
23
|
+
const blockTagPattern =
|
|
24
|
+
/(<\/?(?:address|article|aside|blockquote|div|dl|dt|dd|fieldset|figcaption|figure|footer|form|h[1-6]|header|hr|li|main|nav|ol|p|section|table|tbody|td|tfoot|th|thead|tr|ul)\b[^>]*>)\s+(?=<\/?(?:address|article|aside|blockquote|div|dl|dt|dd|fieldset|figcaption|figure|footer|form|h[1-6]|header|hr|li|main|nav|ol|p|section|table|tbody|td|tfoot|th|thead|tr|ul)\b)/gi;
|
|
25
|
+
|
|
26
|
+
function normalizePosts(
|
|
27
|
+
items: Record<string, PostLoader>,
|
|
28
|
+
): Record<string, PostLoader> {
|
|
29
|
+
return Object.fromEntries(
|
|
30
|
+
Object.entries(items).flatMap(([path, loader]) => {
|
|
31
|
+
const normalized = path.replace(/\\/g, '/');
|
|
32
|
+
|
|
33
|
+
const marker = '/content/';
|
|
34
|
+
const index = normalized.lastIndexOf(marker);
|
|
35
|
+
if (index === -1) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const slug = normalized
|
|
40
|
+
.slice(index + marker.length)
|
|
41
|
+
.replace(/\.md$/, '');
|
|
42
|
+
|
|
43
|
+
return [[slug, loader]];
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const postsAlias = import.meta.glob<string>('#myelophone/app/content/**/*.md', {
|
|
49
|
+
query: '?raw',
|
|
50
|
+
import: 'default',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const postsApp = import.meta.glob<string>('~/content/**/*.md', {
|
|
54
|
+
query: '?raw',
|
|
55
|
+
import: 'default',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const posts = {
|
|
59
|
+
...normalizePosts(postsAlias),
|
|
60
|
+
...normalizePosts(postsApp),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function parseMarkdown(source: string): Post {
|
|
64
|
+
if (!source.startsWith('---')) {
|
|
65
|
+
return {
|
|
66
|
+
meta: {},
|
|
67
|
+
content: source,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
72
|
+
|
|
73
|
+
if (!match) {
|
|
74
|
+
return {
|
|
75
|
+
meta: {},
|
|
76
|
+
content: source,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
meta: parsePostMeta(parse(match[1] ?? '')),
|
|
82
|
+
content: source.slice(match[0].length),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parsePostMeta(value: unknown): PostMeta {
|
|
87
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const source = value as Record<string, unknown>;
|
|
92
|
+
const meta: PostMeta = {};
|
|
93
|
+
|
|
94
|
+
for (const key of ['title', 'description', 'image'] as const) {
|
|
95
|
+
if (typeof source[key] === 'string') {
|
|
96
|
+
meta[key] = source[key];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return meta;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function renderMarkdown(source: string): string {
|
|
104
|
+
return markdown
|
|
105
|
+
.render(source)
|
|
106
|
+
.split(/(<pre\b[\s\S]*?<\/pre>)/gi)
|
|
107
|
+
.map((fragment, index) => {
|
|
108
|
+
if (index % 2 === 1) {
|
|
109
|
+
return fragment;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return fragment
|
|
113
|
+
.replace(/\s*\r?\n\s*/g, ' ')
|
|
114
|
+
.replace(blockTagPattern, '$1')
|
|
115
|
+
.trim();
|
|
116
|
+
})
|
|
117
|
+
.join('');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function getPost(slug: string): Promise<Post | null> {
|
|
121
|
+
const loader = posts[slug];
|
|
122
|
+
|
|
123
|
+
if (!loader) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const cached = postCache.get(slug);
|
|
128
|
+
|
|
129
|
+
if (cached) {
|
|
130
|
+
return cached;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const promise = loader().then(parseMarkdown);
|
|
134
|
+
postCache.set(slug, promise);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
return await promise;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
postCache.delete(slug);
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type JsonLdPrimitive = string | number | boolean | null;
|
|
2
|
+
export type JsonLdValue = JsonLdPrimitive | JsonLdNode | JsonLdValue[];
|
|
3
|
+
|
|
4
|
+
export interface JsonLdNode {
|
|
5
|
+
[key: string]: JsonLdValue | undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface JsonLdHeadOptions {
|
|
9
|
+
key: MaybeRefOrGetter<string>;
|
|
10
|
+
id?: MaybeRefOrGetter<string | undefined>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const compactJsonLd = (value: JsonLdNode): JsonLdNode =>
|
|
14
|
+
Object.fromEntries(
|
|
15
|
+
Object.entries(value).filter(([, entry]) => entry !== undefined),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
export const wrapJsonLdGraph = (
|
|
19
|
+
record: JsonLdNode,
|
|
20
|
+
graph?: JsonLdNode[],
|
|
21
|
+
): JsonLdNode => {
|
|
22
|
+
if (!graph) return record;
|
|
23
|
+
|
|
24
|
+
const { '@context': context = 'https://schema.org', ...node } = record;
|
|
25
|
+
return {
|
|
26
|
+
'@context': context,
|
|
27
|
+
'@graph': [node, ...graph],
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const serializeJsonLd = (value: JsonLdNode): string =>
|
|
32
|
+
JSON.stringify(value)
|
|
33
|
+
.replace(/</g, '\\u003c')
|
|
34
|
+
.replace(/\u2028/g, '\\u2028')
|
|
35
|
+
.replace(/\u2029/g, '\\u2029');
|
|
36
|
+
|
|
37
|
+
export const useJsonLd = (
|
|
38
|
+
schema: MaybeRefOrGetter<JsonLdNode | null>,
|
|
39
|
+
options: JsonLdHeadOptions,
|
|
40
|
+
) => {
|
|
41
|
+
useHead(() => {
|
|
42
|
+
const value = toValue(schema);
|
|
43
|
+
if (!value) return { script: [] };
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
script: [
|
|
47
|
+
{
|
|
48
|
+
key: toValue(options.key),
|
|
49
|
+
id: options.id ? toValue(options.id) : undefined,
|
|
50
|
+
type: 'application/ld+json',
|
|
51
|
+
innerHTML: serializeJsonLd(value),
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type Comparable = string | number | bigint;
|
|
2
|
+
|
|
3
|
+
type ComparableKey<T> = {
|
|
4
|
+
[K in keyof T]-?: T[K] extends Comparable ? K : never;
|
|
5
|
+
}[keyof T];
|
|
6
|
+
|
|
7
|
+
export const sortBy = <T>(
|
|
8
|
+
arr: readonly T[],
|
|
9
|
+
keys: readonly ComparableKey<T>[],
|
|
10
|
+
): T[] =>
|
|
11
|
+
[...arr].sort((a, b) =>
|
|
12
|
+
keys.reduce((res, k) => {
|
|
13
|
+
const av = a[k] as Comparable;
|
|
14
|
+
const bv = b[k] as Comparable;
|
|
15
|
+
|
|
16
|
+
return res || (av > bv ? 1 : av < bv ? -1 : 0);
|
|
17
|
+
}, 0),
|
|
18
|
+
);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export const transliterate = (text: string): string => {
|
|
2
|
+
const customMap: Record<string, string> = {
|
|
3
|
+
а: 'a',
|
|
4
|
+
б: 'b',
|
|
5
|
+
в: 'v',
|
|
6
|
+
г: 'g',
|
|
7
|
+
д: 'd',
|
|
8
|
+
е: 'e',
|
|
9
|
+
ё: 'yo',
|
|
10
|
+
ж: 'zh',
|
|
11
|
+
з: 'z',
|
|
12
|
+
и: 'i',
|
|
13
|
+
й: 'j',
|
|
14
|
+
к: 'k',
|
|
15
|
+
л: 'l',
|
|
16
|
+
м: 'm',
|
|
17
|
+
н: 'n',
|
|
18
|
+
о: 'o',
|
|
19
|
+
п: 'p',
|
|
20
|
+
р: 'r',
|
|
21
|
+
с: 's',
|
|
22
|
+
т: 't',
|
|
23
|
+
у: 'u',
|
|
24
|
+
ф: 'f',
|
|
25
|
+
х: 'kh',
|
|
26
|
+
ц: 'ts',
|
|
27
|
+
ч: 'ch',
|
|
28
|
+
ш: 'sh',
|
|
29
|
+
щ: 'sch',
|
|
30
|
+
ъ: '',
|
|
31
|
+
ы: 'y',
|
|
32
|
+
ь: '',
|
|
33
|
+
э: 'e',
|
|
34
|
+
ю: 'yu',
|
|
35
|
+
я: 'ya',
|
|
36
|
+
ł: 'l',
|
|
37
|
+
α: 'a',
|
|
38
|
+
β: 'b',
|
|
39
|
+
γ: 'g',
|
|
40
|
+
δ: 'd',
|
|
41
|
+
ε: 'e',
|
|
42
|
+
ζ: 'z',
|
|
43
|
+
η: 'h',
|
|
44
|
+
θ: 'th',
|
|
45
|
+
ι: 'i',
|
|
46
|
+
κ: 'k',
|
|
47
|
+
λ: 'l',
|
|
48
|
+
μ: 'm',
|
|
49
|
+
ν: 'n',
|
|
50
|
+
ξ: 'x',
|
|
51
|
+
ο: 'o',
|
|
52
|
+
π: 'p',
|
|
53
|
+
ρ: 'r',
|
|
54
|
+
σ: 's',
|
|
55
|
+
ς: 's',
|
|
56
|
+
τ: 't',
|
|
57
|
+
υ: 'y',
|
|
58
|
+
φ: 'f',
|
|
59
|
+
χ: 'ch',
|
|
60
|
+
ψ: 'ps',
|
|
61
|
+
ω: 'o',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return text
|
|
65
|
+
.toLowerCase()
|
|
66
|
+
.split('')
|
|
67
|
+
.map((c) => customMap[c] || c)
|
|
68
|
+
.join('')
|
|
69
|
+
.normalize('NFD')
|
|
70
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
71
|
+
.trim()
|
|
72
|
+
.replace(/\s+/g, '-')
|
|
73
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
74
|
+
.replace(/-+/g, '-');
|
|
75
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const getYoutubeEmbed = (val?: string) => {
|
|
2
|
+
if (!val || val === 'none' || val.trim() === '') return null;
|
|
3
|
+
|
|
4
|
+
const regExp =
|
|
5
|
+
/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
|
|
6
|
+
const match = val.match(regExp);
|
|
7
|
+
const id = match && match[2] && match[2].length === 11 ? match[2] : val;
|
|
8
|
+
|
|
9
|
+
if (!id) return null;
|
|
10
|
+
|
|
11
|
+
const params = new URLSearchParams({
|
|
12
|
+
autoplay: '1',
|
|
13
|
+
mute: '1',
|
|
14
|
+
controls: '0',
|
|
15
|
+
loop: '1',
|
|
16
|
+
playlist: id,
|
|
17
|
+
playsinline: '1',
|
|
18
|
+
rel: '0',
|
|
19
|
+
modestbranding: '1',
|
|
20
|
+
iv_load_policy: '3',
|
|
21
|
+
autohide: '1',
|
|
22
|
+
disablekb: '1',
|
|
23
|
+
enablejsapi: '1',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return `https://www.youtube.com/embed/${id}?${params.toString()}`;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const getYoutubePoster = (val?: string) => {
|
|
30
|
+
if (!val || val === 'none') return '';
|
|
31
|
+
const regExp =
|
|
32
|
+
/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
|
|
33
|
+
const match = val.match(regExp);
|
|
34
|
+
const id = match && match[2] && match[2].length === 11 ? match[2] : val;
|
|
35
|
+
|
|
36
|
+
return `url('https://img.youtube.com/vi/${id}/maxresdefault.jpg')`;
|
|
37
|
+
};
|