Skip to content

Commit

Permalink
feat: init
Browse files Browse the repository at this point in the history
  • Loading branch information
sxzz committed Sep 2, 2023
0 parents commit 720a690
Show file tree
Hide file tree
Showing 28 changed files with 8,704 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules
*.log*
.nuxt
.nitro
.cache
.output
.env
dist
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
shamefully-hoist = true
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# AST Explorer
26 changes: 26 additions & 0 deletions app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script setup lang="ts">
import { code } from '#imports'
const language = computed(() => {
if (typeof currentLanguage.value.language === 'string') {
return currentLanguage.value.language
}
return currentLanguage.value.language(options.value)
})
</script>

<template>
<main h-screen flex="~ col">
<NavBar mb-1 />
<div min-h-0 flex="~ gap3">
<CodeEditor
v-model="code"
:language="language"
flex-1
max-w="50%"
min-w="50%"
/>
<ASTViewer flex-1 />
</div>
</main>
</template>
70 changes: 70 additions & 0 deletions components/ASTViewer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<script setup lang="ts">
import { getHighlighter } from 'shikiji'
import { hideEmptyKeys, hideLocationData } from '#imports'
const Error = globalThis.Error
let shiki = await getHighlighter({
themes: ['vitesse-dark', 'vitesse-light'],
langs: ['json'],
})
const html = computed(() => {
return shiki.codeToHtml(
JSON.stringify(
ast.value,
(key: string, value: unknown) => {
if (hideEmptyKeys.value && value == null) return undefined
if (
hideLocationData.value &&
['loc', 'start', 'end', ...hideKeys.value].includes(key)
)
return undefined
if (typeof value === 'function') return `function ${value.name}(...)`
return value
},
2
),
{
lang: 'json',
theme: isDark.value ? 'vitesse-dark' : 'vitesse-light',
}
)
})
const hideKeysValue = computed({
get() {
return JSON.stringify(hideKeys.value)
},
set(val) {
hideKeys.value = JSON.parse(val)
},
})
</script>

<template>
<div flex="~ col gap-2" min-w-0>
<div flex="~ gap-2">
<label>
<input type="checkbox" v-model="hideEmptyKeys" /> Hide empty keys
</label>
<label>
<input type="checkbox" v-model="hideLocationData" /> Hide location data
</label>
<label>
Hide keys:
<input
type="input"
v-model="hideKeysValue"
border="~ $c-border"
px1
rounded
/>
</label>
</div>
<div v-if="error" text-red overflow-scroll>
<pre v-text="error instanceof Error ? error.stack : error" />
</div>
<div overflow-scroll v-else v-html="html" />
</div>
</template>
41 changes: 41 additions & 0 deletions components/CodeEditor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script setup lang="ts">
import * as monaco from 'monaco-editor'
import { type MonacoEditor } from '#build/components'
import { type MonacoLanguage } from '~/composables/language'
defineProps<{
language: MonacoLanguage
}>()
const code = defineModel<string>()
const editorRef = shallowRef<InstanceType<typeof MonacoEditor>>()
const options = computed<monaco.editor.IStandaloneEditorConstructionOptions>(
() => {
return {
automaticLayout: true,
theme: isDark.value ? 'vs-dark' : 'vs',
fontSize: 14,
tabSize: 2,
minimap: {
enabled: false,
},
}
}
)
</script>

<template>
<MonacoEditor
v-model="code"
ref="editorRef"
h-full
:lang="language"
:options="options"
>
<div flex="~ col gap-2" w-full h-full items-center justify-center>
<div i-ri:loader-2-line animate-spin text-4xl></div>
<span text-lg>Loading...</span>
</div>
</MonacoEditor>
</template>
41 changes: 41 additions & 0 deletions components/LanguageOptions.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script setup lang="ts">
const dialog = ref<HTMLDialogElement>()
const value = ref(
rawOptions.value ||
JSON.stringify(currentLanguage.value.options.defaultValue, null, 2)
)
function openDialog() {
dialog.value?.showModal()
}
function handleDialogClick(evt: MouseEvent) {
if (evt.target === evt.currentTarget) dialog.value?.close()
}
watchEffect(() => {
rawOptions.value = value.value
})
</script>

<template>
<div flex justify-center items-center>
<button class="i-ri:settings-line" @click="openDialog" />
<dialog ref="dialog" rounded h-50vh p0 @click="handleDialogClick">
<div text-center text-lg py2 font-bold>
Parser Options
<button
class="i-ri:close-line"
p4
float-right
@click="dialog?.close()"
/>
</div>
<CodeEditor
w-50vw
v-model="value"
:language="currentLanguage.options.language"
/>
</dialog>
</div>
</template>
26 changes: 26 additions & 0 deletions components/LanguageSelect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script setup lang="ts">
import { type Language } from '~/composables/language'
const changeLang = (language: Language) => {
currentLanguageId.value = language
}
</script>

<template>
<VMenu :class="{ dark: isDark }" flex>
<button flex="~ gap-1" items-center>
<div :class="currentLanguage.icon" />
{{ currentLanguage.label }}
</button>
<template #popper>
<CommonDropdownItem
v-for="(lang, id) in LANGUAGES"
:key="id"
:icon="lang.icon"
:text="lang.label"
:checked="currentLanguageId === id"
@click="changeLang(id)"
/>
</template>
</VMenu>
</template>
13 changes: 13 additions & 0 deletions components/NavBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<template>
<div p2 flex justify-between items-center>
<div flex="~ gap4">
<h1 font-bold text-lg>AST Viewer</h1>
<LanguageSelect />
<LanguageOptions v-if="currentLanguage.options.configurable" />
</div>

<button @click="toggleDark()">
<div i-ri:sun-line dark:i-ri:moon-line />
</button>
</div>
</template>
26 changes: 26 additions & 0 deletions components/common/dropdown/Dropdown.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script setup lang="ts">
import { dropdownContextKey } from './ctx'
defineProps<{
placement?: string
}>()
const dropdown = ref<any>()
provide(dropdownContextKey, {
hide: () => dropdown.value.hide(),
})
</script>

<template>
<VDropdown
v-bind="$attrs"
ref="dropdown"
:class="{ dark: isDark }"
:placement="placement || 'auto'"
>
<slot />
<template #popper="scope">
<slot name="popper" v-bind="scope" />
</template>
</VDropdown>
</template>
43 changes: 43 additions & 0 deletions components/common/dropdown/DropdownItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script setup lang="ts">
import { dropdownContextKey } from './ctx'
defineProps<{
text?: string
description?: string
icon?: string
checked?: boolean
}>()
const emit = defineEmits(['click'])
const { hide } = inject(dropdownContextKey, undefined) || {}
const el = ref<HTMLDivElement>()
const handleClick = (evt: MouseEvent) => {
hide?.()
emit('click', evt)
}
</script>

<template>
<div
v-bind="$attrs"
ref="el"
flex
gap-2
items-center
cursor-pointer
px3
py2
hover-bg-active
:aria-label="text"
@click="handleClick"
>
<div v-if="icon" :class="icon" />
<slot>
{{ text }}
</slot>

<div v-if="checked" i-ri:check-line />
</div>
</template>
5 changes: 5 additions & 0 deletions components/common/dropdown/ctx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { InjectionKey } from 'vue'

export const dropdownContextKey: InjectionKey<{
hide: () => void
}> = Symbol('dropdownContextKey')
2 changes: 2 additions & 0 deletions composables/dark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const isDark = useDark()
export const toggleDark = useToggle(isDark)
45 changes: 45 additions & 0 deletions composables/language/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import json5 from 'json5'
import * as monaco from 'monaco-editor'
import { javascript } from './javascript'
import { vue } from './vue'

export type MonacoLanguage = 'javascript' | 'typescript' | 'json' | 'vue'
export interface LanguageOption {
label: string
icon: string
language: MonacoLanguage | ((options: any) => MonacoLanguage)
options: {
configurable: boolean
defaultValue: any
language: MonacoLanguage
}
parse(code: string, options: any): any
}

export const LANGUAGES = {
javascript,
vue,
}
export type Language = keyof typeof LANGUAGES

export const currentLanguage = computed(
() => LANGUAGES[currentLanguageId.value] || LANGUAGES.javascript
)

monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
allowComments: true,
enableSchemaRequest: true,
trailingCommas: 'ignore',
})

watchEffect(async () => {
try {
ast.value = currentLanguage.value.parse(
code.value,
json5.parse(rawOptions.value)
)
error.value = null
} catch (err) {
error.value = err
}
})
27 changes: 27 additions & 0 deletions composables/language/javascript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ParserOptions } from '@babel/parser'
import { parse } from '@babel/parser'
import { LanguageOption } from '../language'

// @unocss-include
export const javascript: LanguageOption = {
label: 'JavaScript',
icon: 'i-vscode-icons:file-type-js-official',
language(options: ParserOptions | null | undefined) {
const normalizedPlugins = (options?.plugins || []).map((item) =>
Array.isArray(item) ? item[0] : item
)
if (normalizedPlugins.includes('typescript')) return 'typescript'
return 'javascript'
},
options: {
configurable: true,
defaultValue: {},
language: 'json',
},
parse(code, options: ParserOptions | null | undefined) {
return parse(code, {
sourceType: 'module',
...options,
})
},
}
Loading

0 comments on commit 720a690

Please sign in to comment.