@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.
Files changed (259) hide show
  1. package/.devcontainer/Dockerfile +7 -0
  2. package/.devcontainer/devcontainer.json +21 -0
  3. package/.editorconfig +43 -0
  4. package/.github/workflows/package.yml +73 -0
  5. package/.nuxtignore +6 -0
  6. package/.releaserc.js +132 -0
  7. package/.yarn/releases/yarn-4.18.0.cjs +1000 -0
  8. package/.yarnrc.yml +8 -0
  9. package/CHANGELOG.md +141 -0
  10. package/LICENSE +77 -0
  11. package/README.md +3004 -0
  12. package/app/app.vue +455 -0
  13. package/app/assets/css/animation.css +81 -0
  14. package/app/assets/css/backgrounds.css +57 -0
  15. package/app/assets/css/base.css +182 -0
  16. package/app/assets/css/index.css +9 -0
  17. package/app/assets/css/links.css +12 -0
  18. package/app/assets/css/multimedia.css +102 -0
  19. package/app/assets/css/preloader.css +68 -0
  20. package/app/assets/css/print.css +40 -0
  21. package/app/assets/css/tailwind.css +20 -0
  22. package/app/assets/css/theme.css +29 -0
  23. package/app/components/consent/GoogleMap.vue +59 -0
  24. package/app/components/consent/Youtube.vue +27 -0
  25. package/app/components/cookie/Banner.vue +70 -0
  26. package/app/components/cookie/ConsentWrapper.vue +71 -0
  27. package/app/components/cookie/PrivacyPolicy.vue +179 -0
  28. package/app/components/cookie/SettingsModal.vue +387 -0
  29. package/app/components/grid/Col.vue +262 -0
  30. package/app/components/grid/Container.vue +236 -0
  31. package/app/components/grid/Row.vue +248 -0
  32. package/app/components/grid/Stack.vue +233 -0
  33. package/app/components/myelophone/Copyright.vue +5 -0
  34. package/app/components/myelophone/Welcome.vue +107 -0
  35. package/app/components/page/FullscreenPreloader.vue +177 -0
  36. package/app/components/page/Preloader.vue +18 -0
  37. package/app/components/page/ReadyLocalized.vue +19 -0
  38. package/app/components/ready/.gitkeep +0 -0
  39. package/app/components/safe/Email.vue +52 -0
  40. package/app/components/safe/ImgWithLoader.vue +180 -0
  41. package/app/components/safe/NuxtImg.vue +25 -0
  42. package/app/components/safe/NuxtPicture.vue +29 -0
  43. package/app/components/section/.gitkeep +0 -0
  44. package/app/components/seo/BrandInfo.vue +76 -0
  45. package/app/components/seo/ContentNoIndex.vue +35 -0
  46. package/app/components/seo/NoIndex.vue +22 -0
  47. package/app/components/seo/OrgInfo.vue +130 -0
  48. package/app/components/seo/ProfileInfo.vue +137 -0
  49. package/app/components/ui/Accordion.vue +163 -0
  50. package/app/components/ui/Alert.vue +179 -0
  51. package/app/components/ui/Avatar.vue +88 -0
  52. package/app/components/ui/Badge.vue +150 -0
  53. package/app/components/ui/Banner.vue +133 -0
  54. package/app/components/ui/Button.vue +175 -0
  55. package/app/components/ui/Card.vue +66 -0
  56. package/app/components/ui/Checkbox.vue +134 -0
  57. package/app/components/ui/Chip.vue +136 -0
  58. package/app/components/ui/CommandPalette.vue +434 -0
  59. package/app/components/ui/Console.vue +583 -0
  60. package/app/components/ui/CursorCreative.vue +148 -0
  61. package/app/components/ui/FeatureAccordion.vue +165 -0
  62. package/app/components/ui/FileUpload.vue +705 -0
  63. package/app/components/ui/GeoDependent.vue +72 -0
  64. package/app/components/ui/Heading.vue +51 -0
  65. package/app/components/ui/Icon.vue +42 -0
  66. package/app/components/ui/Icon8.vue +39 -0
  67. package/app/components/ui/Input.vue +88 -0
  68. package/app/components/ui/LangLink.vue +34 -0
  69. package/app/components/ui/LangVisible.vue +33 -0
  70. package/app/components/ui/LanguageSelect.vue +202 -0
  71. package/app/components/ui/LightBox.vue +155 -0
  72. package/app/components/ui/LocalTime.vue +170 -0
  73. package/app/components/ui/Modal.vue +161 -0
  74. package/app/components/ui/Popup.vue +200 -0
  75. package/app/components/ui/PostContent.server.vue +23 -0
  76. package/app/components/ui/ProtectedEmail.vue +52 -0
  77. package/app/components/ui/RelativeTime.vue +141 -0
  78. package/app/components/ui/ScheduledContent.vue +150 -0
  79. package/app/components/ui/ScrollToTop.client.vue +48 -0
  80. package/app/components/ui/SegmentedControl.vue +148 -0
  81. package/app/components/ui/SiteSearch.vue +1191 -0
  82. package/app/components/ui/Skeleton.vue +91 -0
  83. package/app/components/ui/SmartContrast.vue +74 -0
  84. package/app/components/ui/SnapContainer.vue +222 -0
  85. package/app/components/ui/SnapSection.vue +294 -0
  86. package/app/components/ui/SonnerStyle.vue +7 -0
  87. package/app/components/ui/SplitSection.vue +99 -0
  88. package/app/components/ui/Steps.vue +858 -0
  89. package/app/components/ui/StickyWrapper.vue +46 -0
  90. package/app/components/ui/Table.vue +251 -0
  91. package/app/components/ui/Tabs.vue +451 -0
  92. package/app/components/ui/TextColumn.vue +36 -0
  93. package/app/components/ui/Textarea.vue +251 -0
  94. package/app/components/ui/Toggler.vue +40 -0
  95. package/app/components/ui/Tooltip.vue +524 -0
  96. package/app/components/ui/TruncateText.vue +105 -0
  97. package/app/components/ui/ViewportSpacer.vue +72 -0
  98. package/app/components/view/AnimatedCounter.vue +121 -0
  99. package/app/components/view/AnnouncementBar.vue +104 -0
  100. package/app/components/view/AvatarGroup.vue +104 -0
  101. package/app/components/view/Breadcrumbs.vue +97 -0
  102. package/app/components/view/ContentWithToc.vue +152 -0
  103. package/app/components/view/Copyright.vue +68 -0
  104. package/app/components/view/CountdownTimer.vue +184 -0
  105. package/app/components/view/CreativeCTA.vue +143 -0
  106. package/app/components/view/CurrencySelect.vue +254 -0
  107. package/app/components/view/FeatureBoxGrid.vue +142 -0
  108. package/app/components/view/HorizontalMenu.vue +97 -0
  109. package/app/components/view/ImageCompare.vue +141 -0
  110. package/app/components/view/ImgSlider.vue +333 -0
  111. package/app/components/view/InfiniteMarquee.vue +147 -0
  112. package/app/components/view/InfoBar.vue +86 -0
  113. package/app/components/view/LogoGrid.vue +106 -0
  114. package/app/components/view/LogoSlider.vue +83 -0
  115. package/app/components/view/NotFound.vue +81 -0
  116. package/app/components/view/PartialBackground.vue +313 -0
  117. package/app/components/view/QuoteBig.vue +111 -0
  118. package/app/components/view/ResponsiveMenu.vue +219 -0
  119. package/app/components/view/ReviewsSlider.vue +230 -0
  120. package/app/components/view/SocialBar.vue +78 -0
  121. package/app/components/view/StickyHeadingContent.vue +158 -0
  122. package/app/components/view/TallyForm.vue +295 -0
  123. package/app/components/view/WarningOutdated.vue +113 -0
  124. package/app/composables/useApi.ts +88 -0
  125. package/app/composables/useAppSeo.ts +78 -0
  126. package/app/composables/useBreadcrumbs.ts +106 -0
  127. package/app/composables/useChangeUrl.ts +73 -0
  128. package/app/composables/useCommand.ts +28 -0
  129. package/app/composables/useCookieControl.ts +111 -0
  130. package/app/composables/useCookieModal.ts +9 -0
  131. package/app/composables/useDevice.ts +109 -0
  132. package/app/composables/useGeoFetch.ts +139 -0
  133. package/app/composables/useLoadScript.ts +36 -0
  134. package/app/composables/useModalState.ts +37 -0
  135. package/app/composables/useMultiLang.ts +278 -0
  136. package/app/composables/useNetwork.ts +49 -0
  137. package/app/composables/useNuxtVersion.ts +5 -0
  138. package/app/composables/usePiniaStore.ts +5 -0
  139. package/app/composables/usePopupControl.ts +57 -0
  140. package/app/composables/useQuery.ts +307 -0
  141. package/app/composables/useReadyPage.ts +182 -0
  142. package/app/composables/useSafeList.ts +90 -0
  143. package/app/composables/useScriptLoader.ts +221 -0
  144. package/app/composables/useSiteSearch.ts +32 -0
  145. package/app/composables/useSnapScroll.ts +24 -0
  146. package/app/composables/useStatic.ts +5 -0
  147. package/app/composables/useStorage.ts +252 -0
  148. package/app/composables/useStoreBroadcast.ts +146 -0
  149. package/app/composables/useThemeSync.ts +81 -0
  150. package/app/constants/layout.ts +109 -0
  151. package/app/constants/predefinedCookieScripts.ts +1222 -0
  152. package/app/constants/ui-events.ts +1 -0
  153. package/app/error.vue +169 -0
  154. package/app/layouts/default-blog.vue +5 -0
  155. package/app/layouts/default.vue +3 -0
  156. package/app/locales/en/commands.json +5 -0
  157. package/app/locales/en/cookies.json +182 -0
  158. package/app/locales/en/error.json +4 -0
  159. package/app/locales/en/interface.json +33 -0
  160. package/app/locales/en/myelophone.json +8 -0
  161. package/app/locales/en/outdated.json +5 -0
  162. package/app/locales/en/times.json +18 -0
  163. package/app/locales/pl/commands.json +5 -0
  164. package/app/locales/pl/cookies.json +182 -0
  165. package/app/locales/pl/error.json +4 -0
  166. package/app/locales/pl/interface.json +33 -0
  167. package/app/locales/pl/myelophone.json +8 -0
  168. package/app/locales/pl/outdated.json +5 -0
  169. package/app/locales/pl/times.json +18 -0
  170. package/app/locales/ru/commands.json +5 -0
  171. package/app/locales/ru/cookies.json +182 -0
  172. package/app/locales/ru/error.json +4 -0
  173. package/app/locales/ru/interface.json +33 -0
  174. package/app/locales/ru/myelophone.json +8 -0
  175. package/app/locales/ru/outdated.json +5 -0
  176. package/app/locales/ru/times.json +18 -0
  177. package/app/middleware/blog-layout.ts +28 -0
  178. package/app/middleware/i18n.global.ts +17 -0
  179. package/app/middleware/not-found-status.ts +11 -0
  180. package/app/modules/default-blog/index.ts +39 -0
  181. package/app/modules/default-blog/posts.ts +80 -0
  182. package/app/modules/default-sitemap/index.ts +116 -0
  183. package/app/modules/default-sitemap/runtime/sitemap.get.ts +42 -0
  184. package/app/modules/fullscreen-preloader/module.ts +43 -0
  185. package/app/modules/htaccess/.htaccess +38 -0
  186. package/app/modules/htaccess/index.ts +23 -0
  187. package/app/modules/i18n-shaker.mjs +221 -0
  188. package/app/modules/multi18n/module.ts +71 -0
  189. package/app/modules/myelophone-config/module.ts +241 -0
  190. package/app/pages/[...all].vue +9 -0
  191. package/app/pages/index.vue +3 -0
  192. package/app/pages/post/[...slug].vue +30 -0
  193. package/app/plugins/bfcache-refresh.client.ts +9 -0
  194. package/app/plugins/canonical.ts +30 -0
  195. package/app/plugins/csp-nonce.server.ts +41 -0
  196. package/app/plugins/multi18n.client.ts +21 -0
  197. package/app/plugins/multi18n.server.ts +15 -0
  198. package/app/plugins/nuxt-version.client.ts +4 -0
  199. package/app/plugins/restore.client.ts +21 -0
  200. package/app/plugins/reveal.ts +89 -0
  201. package/app/plugins/url-helper.client.ts +66 -0
  202. package/app/server/api/exchange-rates.get.ts +216 -0
  203. package/app/server/api/healthz.ts +1 -0
  204. package/app/server/api/logout.post.ts +6 -0
  205. package/app/server/middleware/normalize-url.ts +14 -0
  206. package/app/server/middleware/security-headers.ts +147 -0
  207. package/app/server/middleware/url-helper.ts +43 -0
  208. package/app/server/plugins/clean-html-spaces.ts +15 -0
  209. package/app/server/plugins/clean-styles.ts +94 -0
  210. package/app/server/plugins/compression.ts +122 -0
  211. package/app/server/plugins/css-preload.ts +26 -0
  212. package/app/server/plugins/early-hints.ts +31 -0
  213. package/app/server/plugins/force-noindex.ts +43 -0
  214. package/app/server/plugins/html-one-line.ts +17 -0
  215. package/app/server/plugins/inline-style.ts +7 -0
  216. package/app/server/routes/healthz.get.ts +1 -0
  217. package/app/server/tsconfig.json +3 -0
  218. package/app/stores/cart.ts +945 -0
  219. package/app/stores/commands.ts +43 -0
  220. package/app/stores/device.ts +156 -0
  221. package/app/stores/querySync.ts +5 -0
  222. package/app/stores/settings.ts +145 -0
  223. package/app/stores/user.ts +607 -0
  224. package/app/types/cookie.ts +89 -0
  225. package/app/types/myelophone.d.ts +87 -0
  226. package/app/types/site-search.ts +110 -0
  227. package/app/utils/asyncRefresh.ts +4 -0
  228. package/app/utils/blog.server.ts +142 -0
  229. package/app/utils/jsonLd.ts +56 -0
  230. package/app/utils/myelophoneConfig.ts +4 -0
  231. package/app/utils/sortBy.ts +18 -0
  232. package/app/utils/transliterate.ts +75 -0
  233. package/app/utils/ytVideo.ts +37 -0
  234. package/app/workers/site-search.worker.ts +661 -0
  235. package/biome.json +94 -0
  236. package/commitlint.config.js +3 -0
  237. package/nuxt.config.ts +539 -0
  238. package/package.json +101 -0
  239. package/playground/content/.gitkeep +0 -0
  240. package/playground/locales/.gitkeep +0 -0
  241. package/playground/myelophone.dev.ts +45 -0
  242. package/playground/nuxt.config.ts +105 -0
  243. package/playground/package.json +5 -0
  244. package/playground/pages/.gitkeep +0 -0
  245. package/playground/tsconfig.json +3 -0
  246. package/playwright.config.ts +23 -0
  247. package/public/browsers/chrome.svg +77 -0
  248. package/public/browsers/edge.svg +98 -0
  249. package/public/browsers/firefox.svg +300 -0
  250. package/public/favicon.ico +0 -0
  251. package/public/myelophone_eng.png +0 -0
  252. package/public/myelophone_eng_white.png +0 -0
  253. package/spa-loading-template.html +72 -0
  254. package/tests/home.spec.ts +9 -0
  255. package/tests/utils.ts +23 -0
  256. package/tsconfig.json +16 -0
  257. package/vite/plugins/clean-css.ts +190 -0
  258. package/vite/plugins/postcss-add-viewportunits.ts +33 -0
  259. 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,4 @@
1
+ // biome-ignore lint/suspicious/noExplicitAny: generic type
2
+ export const useAsyncRefresh = (refresh: (opts?: any) => Promise<void>) => {
3
+ return () => refresh();
4
+ };
@@ -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,4 @@
1
+ import type { MyelophoneClientConfig } from '~/types/myelophone';
2
+
3
+ export const useMyelophoneConfig = (): MyelophoneClientConfig =>
4
+ __MYELOPHONE_CONFIG__;
@@ -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
+ };