Skip to content

Commit

Permalink
Merge branch 'main' into fix-es-locale-sync
Browse files Browse the repository at this point in the history
  • Loading branch information
patak-dev authored Jan 9, 2024
2 parents 9d5dc73 + d8ea685 commit 70dbd50
Show file tree
Hide file tree
Showing 32 changed files with 582 additions and 451 deletions.
1 change: 0 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ dist
.netlify/
.eslintcache

public/shiki
public/emojis

*~
Expand Down
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ NUXT_CLOUDFLARE_ACCOUNT_ID=
NUXT_CLOUDFLARE_NAMESPACE_ID=
NUXT_CLOUDFLARE_API_TOKEN=

# 'cloudflare' | 'fs'
# 'cloudflare' | 'vercel' | 'fs'
NUXT_STORAGE_DRIVER=
NUXT_STORAGE_FS_BASE=

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
run: pnpm nuxi prepare

- name: 🧪 Test project
run: pnpm test
run: pnpm test:ci

- name: 📝 Lint
run: pnpm lint
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ node_modules
*.log
dist
.output
.pnpm-store
.nuxt
.env
.DS_Store
Expand All @@ -11,7 +12,6 @@ dist
.eslintcache
elk-translation-status.json

public/shiki
public/emojis

*~
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ One could put Elk behind popular reverse proxies with SSL Handling like Traefik,
1. adjust permissions of storage dir: ```sudo chown 911:911 ./elk-storage```
1. start container: ```docker-compose up -d```

Note: The provided Dockerfile creates a container which will eventually run Elk as non-root user and create a persistent named Docker volume upon first start (if that volume does not yet exist). This volume is always created with root permission. Failing to change the permissions of ```/elk/data``` inside this volume to UID:GID 911 (as specified for Elk in the Dockerfile) will prevent Elk from storing it's config for user accounts. You either have to fix the permission in the created named volume, or mount a directory with the correct permission to ```/elk/data``` into the container.
> [!NOTE]
> The provided Dockerfile creates a container which will eventually run Elk as non-root user and create a persistent named Docker volume upon first start (if that volume does not yet exist). This volume is always created with root permission. Failing to change the permissions of ```/elk/data``` inside this volume to UID:GID 911 (as specified for Elk in the Dockerfile) will prevent Elk from storing it's config for user accounts. You either have to fix the permission in the created named volume, or mount a directory with the correct permission to ```/elk/data``` into the container.

### Ecosystem
Expand Down Expand Up @@ -151,7 +152,7 @@ You can consult the [PWA documentation](https://docs.elk.zone/pwa) to learn more
- [UnoCSS](https://uno.antfu.me/) - The instant on-demand atomic CSS engine
- [Iconify](https://github.com/iconify/icon-sets#iconify-icon-sets-in-json-format) - Iconify icon sets in JSON format
- [Masto.js](https://neet.github.io/masto.js) - Mastodon API client in TypeScript
- [shiki](https://shiki.matsu.io/) - A beautiful Syntax Highlighter
- [shikiji](https://shikiji.netlify.app/) - A beautiful and powerful syntax highlighter
- [vite-plugin-pwa](https://github.com/vite-pwa/vite-plugin-pwa) - Prompt for update, Web Push Notifications and Web Share Target API

## 👨‍💻 Contributors
Expand Down
4 changes: 3 additions & 1 deletion app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ provideGlobalCommands()
const route = useRoute()
if (process.server && !route.path.startsWith('/settings')) {
const url = useRequestURL()
useHead({
meta: [
{ property: 'og:url', content: `https://elk.zone${route.path}` },
{ property: 'og:url', content: `${url.origin}${route.path}` },
],
})
}
Expand Down
2 changes: 1 addition & 1 deletion components/common/CommonTooltip.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { Popper as VTooltipType } from 'floating-vue/dist'
import type { Popper as VTooltipType } from 'floating-vue'
export interface Props extends Partial<typeof VTooltipType> {
content?: string
Expand Down
30 changes: 30 additions & 0 deletions components/emoji/Emoji.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script setup lang="ts">
const { as, alt, dataEmojiId } = $defineProps<{
as: string
alt?: string
dataEmojiId?: string
}>()
let title = $ref<string | undefined>()
if (alt) {
if (alt.startsWith(':')) {
title = alt.replace(/:/g, '')
}
else {
import('node-emoji').then(({ find }) => {
title = find(alt)?.key.replace(/_/g, ' ')
})
}
}
// if it has a data-emoji-id, use that as the title instead
if (dataEmojiId)
title = dataEmojiId
</script>

<template>
<component :is="as" v-bind="$attrs" :alt="alt" :data-emoji-id="dataEmojiId" :title="title">
<slot />
</component>
</template>
6 changes: 3 additions & 3 deletions components/settings/SettingsThemeColors.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ThemeColors } from '~/composables/settings'
const themes = await import('~/constants/themes.json').then(r => r.default) as [string, ThemeColors][]
const settings = $(useUserSettings())
const currentTheme = $computed(() => settings.themeColors?.['--theme-color-name'] || themes[0][0])
const currentTheme = $computed(() => settings.themeColors?.['--theme-color-name'] || themes[0][1]['--theme-color-name'])
function updateTheme(theme: ThemeColors) {
settings.themeColors = theme
Expand All @@ -19,8 +19,8 @@ function updateTheme(theme: ThemeColors) {
'background': key,
'--local-ring-color': key,
}"
:class="currentTheme === key ? 'ring-2' : 'scale-90'"
:title="key"
:class="currentTheme === theme['--theme-color-name'] ? 'ring-2' : 'scale-90'"
:title="theme['--theme-color-name']"
w-8 h-8 rounded-full transition-all
ring="$local-ring-color offset-3 offset-$c-bg-base"
@click="updateTheme(theme)"
Expand Down
2 changes: 1 addition & 1 deletion components/status/StatusSpoiler.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function getToggleText() {
<slot name="spoiler" />
</div>
<div flex="~ gap-1 center" w-full :mb="isDM && !showContent ? '4' : ''" mt="-4.5">
<button btn-text px-2 py-1 :bg="isDM ? 'transparent' : 'base'" flex="~ center gap-2" :class="showContent ? '' : 'filter-saturate-0 hover:filter-saturate-100'" :aria-expanded="showContent" @click="toggleContent()">
<button btn-text px-2 py-1 rounded-lg :bg="isDM ? 'transparent' : 'base'" flex="~ center gap-2" :class="showContent ? '' : 'filter-saturate-0 hover:filter-saturate-100'" :aria-expanded="showContent" @click="toggleContent()">
<div v-if="showContent" i-ri:eye-line />
<div v-else i-ri:eye-close-line />
{{ showContent ? $t('status.spoiler_show_less') : $t(getToggleText()) }}
Expand Down
6 changes: 4 additions & 2 deletions components/tag/TagCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ function go(evt: MouseEvent | KeyboardEvent) {
<div>
<h4 flex items-center text-size-base leading-normal font-medium line-clamp-1 break-all ws-pre-wrap>
<TagActionButton :tag="tag" />
<span>#</span>
<span hover:underline>{{ tag.name }}</span>
<bdi>
<span>#</span>
<span hover:underline>{{ tag.name }}</span>
</bdi>
</h4>
<CommonTrending v-if="tag.history" :history="tag.history" text-sm text-secondary line-clamp-1 ws-pre-wrap break-all />
</div>
Expand Down
4 changes: 4 additions & 0 deletions composables/content-parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ const sanitizer = sanitize({
/**
* Parse raw HTML form Mastodon server to AST,
* with interop of custom emojis and inline Markdown syntax
* @param html The content to parse
* @param options The parsing options
*/
export function parseMastodonHTML(
html: string,
Expand Down Expand Up @@ -140,6 +142,8 @@ export function parseMastodonHTML(

/**
* Converts raw HTML form Mastodon server to HTML for Tiptap editor
* @param html The content to parse
* @param customEmojis The custom emojis to use
*/
export function convertMastodonHTML(html: string, customEmojis: Record<string, mastodon.v1.CustomEmoji> = {}) {
const tree = parseMastodonHTML(html, {
Expand Down
42 changes: 38 additions & 4 deletions composables/content-render.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { TEXT_NODE } from 'ultrahtml'
import type { Node } from 'ultrahtml'
import { ELEMENT_NODE, TEXT_NODE } from 'ultrahtml'
import type { ElementNode, Node } from 'ultrahtml'
import { Fragment, h, isVNode } from 'vue'
import type { VNode } from 'vue'
import { RouterLink } from 'vue-router'
import { decode } from 'tiny-decode'
import type { ContentParseOptions } from './content-parse'
import { parseMastodonHTML } from './content-parse'
import Emoji from '~/components/emoji/Emoji.vue'
import ContentCode from '~/components/content/ContentCode.vue'
import ContentMentionGroup from '~/components/content/ContentMentionGroup.vue'
import AccountHoverWrapper from '~/components/account/AccountHoverWrapper.vue'
Expand All @@ -19,8 +20,11 @@ function getTextualAstComponents(astChildren: Node[]): string {
}

/**
* Raw HTML to VNodes
*/
* Raw HTML to VNodes.
*
* @param content HTML content.
* @param options Options.
*/
export function contentToVNode(
content: string,
options?: ContentParseOptions,
Expand All @@ -43,6 +47,17 @@ export function nodeToVNode(node: Node): VNode | string | null {
if (node.name === 'mention-group')
return h(ContentMentionGroup, node.attributes, () => node.children.map(treeToVNode))

// add tooltip to emojis
if (node.name === 'picture' || (node.name === 'img' && node.attributes?.alt)) {
const props = node.attributes ?? {}
props.as = node.name
return h(
Emoji,
props,
() => node.children.map(treeToVNode),
)
}

if ('children' in node) {
if (node.name === 'a' && (node.attributes.href?.startsWith('/') || node.attributes.href?.startsWith('.'))) {
node.attributes.to = node.attributes.href
Expand Down Expand Up @@ -83,6 +98,23 @@ function treeToVNode(
return null
}

function addBdiNode(node: Node) {
if (node.children.length === 1 && node.children[0].type === ELEMENT_NODE && node.children[0].name === 'bdi')
return

const children = node.children.splice(0, node.children.length)
const bdi = {
name: 'bdi',
parent: node,
loc: node.loc,
type: ELEMENT_NODE,
attributes: {},
children,
} satisfies ElementNode
children.forEach((n: Node) => n.parent = bdi)
node.children.push(bdi)
}

function handleMention(el: Node) {
// Redirect mentions to the user page
if (el.name === 'a' && el.attributes.class?.includes('mention')) {
Expand All @@ -93,11 +125,13 @@ function handleMention(el: Node) {
const [, server, username] = matchUser
const handle = `${username}@${server.replace(/(.+\.)(.+\..+)/, '$2')}`
el.attributes.href = `/${server}/@${username}`
addBdiNode(el)
return h(AccountHoverWrapper, { handle, class: 'inline-block' }, () => nodeToVNode(el))
}
const matchTag = href.match(TagLinkRE)
if (matchTag) {
const [, , name] = matchTag
addBdiNode(el)
el.attributes.href = `/${currentServer.value}/tags/${name}`
}
}
Expand Down
1 change: 1 addition & 0 deletions composables/masto/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const accountFieldIcons: Record<string, string> = Object.fromEntries(Obje
Switch: 'i-ri:switch-line',
Telegram: 'i-ri:telegram-line',
Threads: 'i-ri:threads-line',
TikTok: 'i-ri:tiktok-line',
Tumblr: 'i-ri:tumblr-line',
Twitch: 'i-ri:twitch-line',
Twitter: 'i-ri:twitter-line',
Expand Down
45 changes: 21 additions & 24 deletions composables/shiki.ts → composables/shikiji.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import type { Highlighter, Lang } from 'shiki-es'
import { type Highlighter, type BuiltinLanguage as Lang } from 'shikiji'

const shiki = ref<Highlighter>()
const highlighter = ref<Highlighter>()

const registeredLang = ref(new Map<string, boolean>())
let shikiImport: Promise<void> | undefined
let shikijiImport: Promise<void> | undefined

export function useHighlighter(lang: Lang) {
if (!shikiImport) {
shikiImport = import('shiki-es')
.then(async (r) => {
r.setCDN('/shiki/')
shiki.value = await r.getHighlighter({
export function useHighlighter(lang: Lang): { promise?: Promise<void>; highlighter?: Highlighter } {
if (!shikijiImport) {
shikijiImport = import('shikiji')
.then(async ({ getHighlighter }) => {
highlighter.value = await getHighlighter({
themes: [
'vitesse-dark',
'vitesse-light',
Expand All @@ -22,29 +21,31 @@ export function useHighlighter(lang: Lang) {
],
})
})

return { promise: shikijiImport }
}

if (!shiki.value)
return undefined
if (!highlighter.value)
return { promise: shikijiImport }

if (!registeredLang.value.get(lang)) {
shiki.value.loadLanguage(lang)
const promise = highlighter.value.loadLanguage(lang)
.then(() => {
registeredLang.value.set(lang, true)
})
.catch(() => {
const fallbackLang = 'md'
shiki.value?.loadLanguage(fallbackLang).then(() => {
highlighter.value?.loadLanguage(fallbackLang).then(() => {
registeredLang.value.set(fallbackLang, true)
})
})
return undefined
return { promise }
}

return shiki.value
return { highlighter: highlighter.value }
}

export function useShikiTheme() {
function useShikijiTheme() {
return useColorMode().value === 'dark' ? 'vitesse-dark' : 'vitesse-light'
}

Expand All @@ -61,16 +62,12 @@ function escapeHtml(text: string) {
}

export function highlightCode(code: string, lang: Lang) {
const shiki = useHighlighter(lang)
if (!shiki)
const { highlighter } = useHighlighter(lang)
if (!highlighter)
return escapeHtml(code)

return shiki.codeToHtml(code, {
return highlighter.codeToHtml(code, {
lang,
theme: useShikiTheme(),
theme: useShikijiTheme(),
})
}

export function useShiki() {
return shiki
}
4 changes: 2 additions & 2 deletions composables/tiptap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Plugin } from 'prosemirror-state'

import type { Ref } from 'vue'
import { TiptapEmojiSuggestion, TiptapHashtagSuggestion, TiptapMentionSuggestion } from './tiptap/suggestion'
import { TiptapPluginCodeBlockShiki } from './tiptap/shiki'
import { TiptapPluginCodeBlockShikiji } from './tiptap/shikiji'
import { TiptapPluginCustomEmoji } from './tiptap/custom-emoji'
import { TiptapPluginEmoji } from './tiptap/emoji'

Expand Down Expand Up @@ -70,7 +70,7 @@ export function useTiptap(options: UseTiptapOptions) {
Placeholder.configure({
placeholder: () => placeholder.value!,
}),
TiptapPluginCodeBlockShiki,
TiptapPluginCodeBlockShikiji,
History.configure({
depth: 10,
}),
Expand Down
Loading

0 comments on commit 70dbd50

Please sign in to comment.