Skip to content
Closed
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
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules
*.md
dist
.git*
.vscode
.env
.DS_Store
test
22 changes: 18 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ jobs:
- name: Checkout
uses: actions/checkout@v5

- name: Setup QEMU
uses: docker/setup-qemu-action@v3

- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Install pnpm
uses: pnpm/action-setup@v4

Expand All @@ -27,8 +33,16 @@ jobs:
- name: Install dependencies
run: pnpm install

- name: Lint
run: pnpm run lint
- name: Build docker image for tests
run: docker build -t texo-test --target development .

- name: Lint in docker
run: docker run --rm texo-test pnpm run lint

- name: Typecheck in docker
run: docker run --rm texo-test pnpm run typecheck

- name: Typecheck
run: pnpm run typecheck
- name: Test in docker
env:
FORCE_REAL_PANDOC: 1
run: docker run --rm -e FORCE_REAL_PANDOC texo-test pnpm vitest run
43 changes: 43 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
FROM pandoc/core:latest AS base
RUN apk add --no-cache curl nodejs npm git \
&& npm install -g pnpm

ENTRYPOINT []

WORKDIR /app

FROM base AS deps
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

# Usage: docker build --target development -t my-app-dev .
FROM base AS development
ENV NODE_ENV=development
COPY --from=deps /app/node_modules ./node_modules
COPY . .

EXPOSE 3000
CMD ["pnpm", "dev"]

# Usage: docker build --target test .
FROM development AS test
ENV NODE_ENV=test
RUN pnpm test

FROM base AS build
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm build

FROM base AS prod-deps
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --prod --frozen-lockfile

FROM base AS runtime
ENV NODE_ENV=production

COPY --from=prod-deps /app/node_modules /app/node_modules
COPY --from=build /app/.output /app/.output

EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]
63 changes: 61 additions & 2 deletions app/composables/textProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { tex2typst } from 'tex2typst'
import { Latex2TypstTool } from './types/pandoc'

/**
* 包裹 LaTeX 代码
Expand Down Expand Up @@ -40,7 +41,65 @@ export function formatLatex(code: string): string {
return new_tokens.join('')
}

export function convertToTypst(code: string) {
let lastInput: string | null = null
let lastOutput: string | null = null
let clientModeOverride: boolean | null = null

function isClientRuntime(): boolean {
if (clientModeOverride !== null) {
return clientModeOverride
}
return import.meta.client
}

export async function convertToTypst(code: string, tool: Latex2TypstTool = Latex2TypstTool.Pandoc): Promise<string> {
const cleanedCode = code.replace(/~/g, '\\ ')
return tex2typst(cleanedCode)
const cacheKey = `${tool}:${cleanedCode.trim()}`

if (isClientRuntime()){
if (lastInput === cacheKey && lastOutput !== null) {
console.log(`[convertToTypst] Cache hit: same input as last conversion`)
return lastOutput
}
}
let result: string
switch (tool) {
case Latex2TypstTool.Pandoc:
const response = await $fetch<{ success: boolean; output: string }>('/api/convert/typst', {
method: 'POST',
body: { latex: cleanedCode }
})
if (response.success) {
result = response.output
} else {
throw new Error('Pandoc conversion failed')
}
break
case Latex2TypstTool.Tex2Typst:
result = tex2typst(cleanedCode)
break
default:
throw new Error(`Unknown conversion tool: ${tool}`)
}

if (isClientRuntime()){
lastInput = cacheKey
lastOutput = result
}
return result
}

/**
* Test helper: reset cached conversion state.
*/
export function __resetConvertCache(): void {
lastInput = null
lastOutput = null
}

/**
* Test helper: override client runtime detection.
*/
export function __setConvertClientMode(value: boolean | null): void {
clientModeOverride = value
}
20 changes: 20 additions & 0 deletions app/composables/types/pandoc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export enum PandocStatus {
Ready = 'ready',
Processing = 'processing',
Result = 'result',
Error = 'error',
Timeout = 'timeout'
}

export type PandocConvertOption = {
from?: string
to?: string
extraArgs?: string[]
timeout?: number
key?: string
}

export enum Latex2TypstTool {
Tex2Typst = 'tex2typst',
Pandoc = 'pandoc',
}
2 changes: 1 addition & 1 deletion app/pages/ocr.vue
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function clear() {

async function copyAsTypst() {
try {
const typstCode = convertToTypst(latexCode.value)
const typstCode = await convertToTypst(latexCode.value)
await navigator.clipboard.writeText(typstCode)
toast?.add({
title: t('typstCode') + ' ' + t('copied'),
Expand Down
15 changes: 15 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
services:
app:
build:
context: .
target: development
volumes:
- .:/app
- /app/node_modules
environment:
- HOST=0.0.0.0
- NUXT_HOST=0.0.0.0
- PORT=3000
- NODE_ENV=development
ports:
- "3000:3000"
3 changes: 2 additions & 1 deletion nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ export default defineNuxtConfig({
'@nuxt/eslint',
'@nuxt/ui',
'@nuxtjs/i18n',
'@vite-pwa/nuxt'
'@vite-pwa/nuxt',
'@nuxt/test-utils/module',
],
ssr: true,
components: true,
Expand Down
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"lint": "eslint .",
"typecheck": "nuxt typecheck"
"typecheck": "nuxt typecheck",
"test:nuxt": "npx vitest run --project nuxt",
"test": "npx vitest run"
},
"dependencies": {
"@giscus/vue": "^3.1.1",
Expand All @@ -22,12 +24,18 @@
"image-js": "^1.0.0",
"katex": "^0.16.23",
"nuxt": "^4.1.2",
"p-limit": "^7.2.0",
"tailwindcss": "^4.1.17",
"tex2typst": "^0.4.0"
},
"devDependencies": {
"@nuxt/eslint": "^1.9.0",
"@nuxt/test-utils": "^3.20.1",
"@vue/test-utils": "^2.4.6",
"eslint": "^9.37.0",
"happy-dom": "^20.0.10",
"typescript": "^5.9.3",
"vitest": "^4.0.10",
"vue-tsc": "^3.1.0"
},
"resolutions": {
Expand Down
Loading