Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI Component MarkdownView for code editor #1109

Draft
wants to merge 2 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 22 additions & 13 deletions spx-gui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions spx-gui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,25 @@
"eslint-plugin-vue": "^9.20.1",
"file-saver": "^2.0.5",
"happy-dom": "^14.3.6",
"hast": "^1.0.0",
"hast-util-raw": "^9.1.0",
"hast-util-sanitize": "^5.0.2",
"install": "^0.13.0",
"jszip": "^3.10.1",
"jwt-decode": "^4.0.0",
"konva": "^9.3.1",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
"mdast-util-from-markdown": "^2.0.2",
"mdast-util-to-hast": "^13.2.0",
"monaco-editor": "^0.45.0",
"naive-ui": "^2.38.1",
"nanoid": "^5.0.5",
"npm": "^10.3.0",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"prettier": "^3.2.2",
"property-information": "^6.5.0",
"qiniu-js": "^3.4.2",
"sass": "^1.69.7",
"scss": "^0.2.4",
Expand Down
142 changes: 142 additions & 0 deletions spx-gui/src/components/common/markdown-vue/MarkdownView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { computed, defineComponent, h, type VNode, type Component } from 'vue'
import { fromMarkdown } from 'mdast-util-from-markdown'
import { html, find } from 'property-information'
import type * as hast from 'hast'
import { toHast } from 'mdast-util-to-hast'
import { raw } from 'hast-util-raw'
import { defaultSchema, sanitize, type Schema } from 'hast-util-sanitize'

export type Props = {
/** Markdown string */
value: string
/**
* Custom components (key as component name expected to be in kebab-case).
* Example:
* ```js
* {
* 'my-comp1': MyComp1,
* 'my-comp2': MyComp2
* }
* ```
* Usage in markdown (prop name expected to be in kebab-case):
* ```markdown
* <my-comp1 prop1="value1" prop2="value2" />
* <my-comp2 prop1="value1" prop2="value2">
* Content
* </my-comp2>
* ```
*/
components?: Record<string, Component>
}

export default defineComponent<Props>(
(props, { attrs }) => {
const hastNodes = computed(() => {
const components = props.components ?? {}
const mdast = fromMarkdown(props.value)
const hast = toHast(mdast, { allowDangerousHtml: true })
const rawProcessed = raw(hast, { tagfilter: false })
// return rawProcessed
const sanitizeSchema: Schema = {
tagNames: (defaultSchema.tagNames ?? []).concat(...Object.keys(components)),
attributes: {
...defaultSchema.attributes,
...Object.entries(components).reduce(
(attrs, [tagName, component]) => {
attrs[tagName] = getComponentPropNames(component).map(camelCase2KebabCase)
return attrs
},
{} as Record<string, string[]>
)
}
}
return sanitize(rawProcessed, sanitizeSchema)
})
return function render() {
return renderHastNodes(hastNodes.value, attrs, props.components ?? {})
}
},
{
name: 'MarkdownView',
props: {
value: {
type: String,
required: true
},
components: {
type: Object,
required: false,
default: () => ({})
}
}
}
)

function renderHastNodes(node: hast.Nodes, attrs: Record<string, unknown>, components: Record<string, Component>) {
if (node.type === 'root') {
return h(
'div',
attrs,
node.children.map((child) => renderHastNode(child, components))
)
}
return h('div', attrs, [renderHastNode(node, components)])
}

type VRendered = VNode | string | null | VRendered[]

function renderHastNode(node: hast.Node, components: Record<string, Component>): VRendered {
switch (node.type) {
case 'element':
return renderElement(node as hast.Element, components)
case 'text':
return (node as hast.Text).value
default:
return null
}
}

function renderElement(element: hast.Element, components: Record<string, Component>): VNode {
const props = adaptProperties(element.properties)
let type: string | Component
let children: VRendered | (() => VRendered)
if (Object.prototype.hasOwnProperty.call(components, element.tagName)) {
type = components[element.tagName]
// Use function slot for custom components to avoid Vue warning:
// [Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.
children = () => element.children.map((c) => renderHastNode(c, components))
} else {
type = element.tagName
children = element.children.map((c) => renderHastNode(c, components))
}
return h(type, props, children)
}

function adaptProperties(properties: hast.Properties) {
return Object.keys(properties).reduce(
(props, key) => {
const value = properties[key]
if (value == null) return props
const propertyInfo = find(html, key)
const newKey = propertyInfo.attribute
const newValue = Array.isArray(value) ? value[0] : value
props[newKey] = newValue
return props
},
{} as Record<string, string | boolean | number>
)
}

function camelCase2KebabCase(str: string) {
return str
.replace(/([A-Z])/g, '-$1')
.replace(/^-/, '')
.toLowerCase()
}

function getComponentPropNames(compDef: Component) {
const props = (compDef as any).props
if (props == null) return []
if (Array.isArray(props)) return props
return Object.keys(props)
}
6 changes: 6 additions & 0 deletions spx-gui/src/components/common/markdown-vue/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Using Markdown with Vue

- Render markdown in Vue components
- Integrate Vue components in markdown
- Seamless cooperation between nested and parent Vue components
- In-browser parsing and rendering
11 changes: 8 additions & 3 deletions spx-gui/src/components/editor/EditorContextProvider.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

<script lang="ts">
import { inject } from 'vue'
import { Runtime } from '@/models/runtime'

export type EditorCtx = {
project: Project
userInfo: UserInfo
runtime: Runtime
}

const editorCtxKey: InjectionKey<EditorCtx> = Symbol('editor-ctx')
Expand All @@ -23,16 +25,19 @@ export function useEditorCtx() {
import { provide, type InjectionKey } from 'vue'
import { Project } from '@/models/project'
import type { UserInfo } from '@/stores/user'
import { computedShallowReactive } from '@/utils/utils'
import { computedShallowReactive, useComputedDisposable } from '@/utils/utils'

const props = defineProps<{
project: Project
userInfo: UserInfo
}>()

const editorCtx = computedShallowReactive(() => ({
const runtimeRef = useComputedDisposable(() => new Runtime(props.project))

const editorCtx = computedShallowReactive<EditorCtx>(() => ({
project: props.project,
userInfo: props.userInfo
userInfo: props.userInfo,
runtime: runtimeRef.value
}))

provide(editorCtxKey, editorCtx)
Expand Down
4 changes: 1 addition & 3 deletions spx-gui/src/components/editor/code-editor/CodeEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { useEditorCtx } from '../EditorContextProvider.vue'
import { Copilot } from './copilot'
import { DocumentBase } from './document-base'
import { Spxlc } from './lsp'
import { Runtime } from './runtime'
import {
CodeEditorUIComp,
type ICodeEditorUI,
Expand All @@ -24,7 +23,6 @@ function initialize(ui: ICodeEditorUI) {
const copilot = new Copilot()
const documentBase = new DocumentBase()
const spxlc = new Spxlc()
const runtime = new Runtime()

ui.registerAPIReferenceProvider({
async provideAPIReference(ctx, position) {
Expand Down Expand Up @@ -66,7 +64,7 @@ function initialize(ui: ICodeEditorUI) {
implements IDiagnosticsProvider
{
async provideDiagnostics(ctx: DiagnosticsContext): Promise<Diagnostic[]> {
console.warn('TODO', ctx, runtime)
console.warn('TODO', ctx, editorCtx.runtime)
return []
}
}
Expand Down
2 changes: 1 addition & 1 deletion spx-gui/src/components/editor/code-editor/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export type MarkdownStringFlag = 'basic' | 'advanced'
* We use flag to distinguish different types of Markdown string.
* Different types of Markdown string expect different rendering behaviors, especially for custom components support in MDX.
*/
export type MarkdownString<F extends MarkdownStringFlag> = {
export type MarkdownString<F extends MarkdownStringFlag = MarkdownStringFlag> = {
flag?: F
/** Markdown string with MDX support. */
value: string | LocaleMessage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@ export const forIterate: DefinitionDocumentationItem = {
definition: { name: 'for_iterate (TODO)' },
insertText: 'for ${1:i}, ${2:v} <- ${3:set} {\n\t${4:}\n}',
overview: 'for i, v <- set {} (TODO)',
detail: makeBasicMarkdownString({ en: 'Iterate within given set', zh: '遍历指定集合' })
detail: makeBasicMarkdownString({
en: 'Iterate within given set, e.g., `for i, v <- set {}`',
zh: '遍历指定集合,示例:`for i, v <- set {}`'
})
}
10 changes: 8 additions & 2 deletions spx-gui/src/components/editor/code-editor/document-base/spx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ export const onStart: DefinitionDocumentationItem = {
},
insertText: 'onStart => {\n\t${1}\n}',
overview: 'func onStart(callback func())',
detail: makeBasicMarkdownString({ en: 'Listen to game start', zh: '游戏开始时执行' })
detail: makeBasicMarkdownString({
en: 'Listen to game start, e.g., `onStart => {}`',
zh: '游戏开始时执行,示例:`onStart => {}`'
})
}

export const setXYpos: DefinitionDocumentationItem = {
Expand All @@ -29,5 +32,8 @@ export const setXYpos: DefinitionDocumentationItem = {
},
insertText: 'setXYpos ${1:X}, ${2:Y}',
overview: 'func setXYpos(x, y float64)',
detail: makeBasicMarkdownString({ en: 'Move to given position', zh: '移动到指定位置' })
detail: makeBasicMarkdownString({
en: 'Move to given position, e.g., `setXYpos 0, 0`',
zh: '移动到指定位置,示例:`setXYpos 0, 0`'
})
}
23 changes: 0 additions & 23 deletions spx-gui/src/components/editor/code-editor/runtime.ts

This file was deleted.

Loading
Loading