Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b7da8296c | |||
| e6773c6baf | |||
| 2c8dad08f6 | |||
| 7fecaf491a | |||
| 9c7a10aa39 | |||
| bbf532d282 | |||
| eada1e9979 | |||
| 2f88ad124d | |||
| a398127522 | |||
| 083b195462 | |||
| 0656ddfe52 | |||
| 056d9a7f47 | |||
| 8ed44cc352 | |||
| 52ebff6286 | |||
| 7c0ec9dd39 | |||
| e9a34a5bd8 | |||
| f9fc0d9905 | |||
| d7ca7b194d | |||
| 47cf84f20b | |||
| 2d3e64da92 | |||
| 2db6e63162 | |||
| f2b7034b51 | |||
| 4c6032bce0 | |||
| 2fdb155547 | |||
| 75bc3bcc73 | |||
| 1b773fdda0 | |||
| 019b4041a8 | |||
| 4aca4aaac6 | |||
| f9d46aabeb | |||
| 54ab282efc | |||
| f976c5ba92 | |||
| f5a94c0ced |
@@ -26,13 +26,12 @@ cypress/results/
|
|||||||
# Ace themes for conversion
|
# Ace themes for conversion
|
||||||
frontend/js/features/source-editor/themes/ace/
|
frontend/js/features/source-editor/themes/ace/
|
||||||
|
|
||||||
# Compiled parser files
|
# Compiled parser files (latex/bibtex are generated by webpack plugin at build time)
|
||||||
frontend/js/features/source-editor/lezer-latex/latex.mjs
|
frontend/js/features/source-editor/lezer-latex/latex.mjs
|
||||||
frontend/js/features/source-editor/lezer-latex/latex.terms.mjs
|
frontend/js/features/source-editor/lezer-latex/latex.terms.mjs
|
||||||
frontend/js/features/source-editor/lezer-bibtex/bibtex.mjs
|
frontend/js/features/source-editor/lezer-bibtex/bibtex.mjs
|
||||||
frontend/js/features/source-editor/lezer-bibtex/bibtex.terms.mjs
|
frontend/js/features/source-editor/lezer-bibtex/bibtex.terms.mjs
|
||||||
frontend/js/features/source-editor/lezer-typst/typst.mjs
|
# typst compiled files are committed (generated via node scripts/lezer-latex/generate.mjs)
|
||||||
frontend/js/features/source-editor/lezer-typst/typst.terms.mjs
|
|
||||||
|
|
||||||
!**/fixtures/**/*.log
|
!**/fixtures/**/*.log
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { pipeline } from 'node:stream/promises'
|
import { pipeline } from 'node:stream/promises'
|
||||||
import Metrics from '@overleaf/metrics'
|
import Metrics from '@overleaf/metrics'
|
||||||
import ProjectGetter from '../Project/ProjectGetter.mjs'
|
import ProjectGetter from '../Project/ProjectGetter.mjs'
|
||||||
|
import { Project } from '../../models/Project.mjs'
|
||||||
import CompileManager from './CompileManager.mjs'
|
import CompileManager from './CompileManager.mjs'
|
||||||
import ClsiManager from './ClsiManager.mjs'
|
import ClsiManager from './ClsiManager.mjs'
|
||||||
import logger from '@overleaf/logger'
|
import logger from '@overleaf/logger'
|
||||||
@@ -8,6 +9,7 @@ import Settings from '@overleaf/settings'
|
|||||||
import Errors from '../Errors/Errors.js'
|
import Errors from '../Errors/Errors.js'
|
||||||
import SessionManager from '../Authentication/SessionManager.mjs'
|
import SessionManager from '../Authentication/SessionManager.mjs'
|
||||||
import { userCanInstallPython } from './PythonVenvGate.mjs'
|
import { userCanInstallPython } from './PythonVenvGate.mjs'
|
||||||
|
import TokenAccessHandler from '../TokenAccess/TokenAccessHandler.mjs'
|
||||||
import { RateLimiter } from '../../infrastructure/RateLimiter.mjs'
|
import { RateLimiter } from '../../infrastructure/RateLimiter.mjs'
|
||||||
import Validation from '../../infrastructure/Validation.mjs'
|
import Validation from '../../infrastructure/Validation.mjs'
|
||||||
import Path from 'node:path'
|
import Path from 'node:path'
|
||||||
@@ -205,7 +207,8 @@ const _CompileController = {
|
|||||||
// Allow building a per-project Python venv from requirements.txt only for
|
// Allow building a per-project Python venv from requirements.txt only for
|
||||||
// the project owner and invited collaborators — never anonymous or
|
// the project owner and invited collaborators — never anonymous or
|
||||||
// link-sharing users.
|
// link-sharing users.
|
||||||
options.allowPythonInstall = await userCanInstallPython(userId, projectId)
|
const anonToken = TokenAccessHandler.getRequestToken(req, projectId)
|
||||||
|
options.allowPythonInstall = await userCanInstallPython(userId, projectId, anonToken)
|
||||||
|
|
||||||
let {
|
let {
|
||||||
enablePdfCaching,
|
enablePdfCaching,
|
||||||
@@ -300,6 +303,26 @@ const _CompileController = {
|
|||||||
? getOutputFilesArchiveSpecification(projectId, userId, buildId)
|
? getOutputFilesArchiveSpecification(projectId, userId, buildId)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
|
// Persist quarto output flavor so the project-list badge can distinguish
|
||||||
|
// RevealJS presentations from PDF documents without needing a compile.
|
||||||
|
// options.compiler is not sent by the frontend, so we read the stored
|
||||||
|
// compiler from the DB. Done fire-and-forget so it never delays the response.
|
||||||
|
if (status === 'success') {
|
||||||
|
const isHtml = outputFiles.some(f => f.path === 'output.html')
|
||||||
|
ProjectGetter.promises
|
||||||
|
.getProject(projectId, { compiler: 1 })
|
||||||
|
.then(project => {
|
||||||
|
if (project?.compiler !== 'quarto') return
|
||||||
|
return Project.updateOne(
|
||||||
|
{ _id: projectId },
|
||||||
|
{ quartoFlavor: isHtml ? 'revealjs' : 'pdf' }
|
||||||
|
).exec()
|
||||||
|
})
|
||||||
|
.catch(err =>
|
||||||
|
logger.warn({ err, projectId }, 'failed to update quartoFlavor')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
status,
|
status,
|
||||||
outputFiles,
|
outputFiles,
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import AuthorizationManager from '../Authorization/AuthorizationManager.mjs'
|
|||||||
|
|
||||||
// Whether this user may have the compiler install a project's requirements.txt
|
// Whether this user may have the compiler install a project's requirements.txt
|
||||||
// into a cached venv (so Quarto's Python cells can use libraries beyond the
|
// into a cached venv (so Quarto's Python cells can use libraries beyond the
|
||||||
// bundled base set). Gated to the project owner + invited collaborators (any
|
// bundled base set). Allowed for any user who can access the project — owner,
|
||||||
// role): ignorePublicAccess excludes link-sharing/public and anonymous users,
|
// invited collaborators, token-link users, and public-project readers — since
|
||||||
// who fall back to the base Python interpreter. Returns false when the feature
|
// the set of packages to install is already controlled by requirements.vrf
|
||||||
// is disabled or the privilege check fails.
|
// (writable only by project members with write access). Returns false when the
|
||||||
export async function userCanInstallPython(userId, projectId) {
|
// feature is disabled, the privilege check fails, or the user has no access.
|
||||||
|
export async function userCanInstallPython(userId, projectId, token = null) {
|
||||||
if (!Settings.enableProjectPythonVenv) {
|
if (!Settings.enableProjectPythonVenv) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -17,8 +18,7 @@ export async function userCanInstallPython(userId, projectId) {
|
|||||||
await AuthorizationManager.promises.getPrivilegeLevelForProject(
|
await AuthorizationManager.promises.getPrivilegeLevelForProject(
|
||||||
userId,
|
userId,
|
||||||
projectId,
|
projectId,
|
||||||
null,
|
token
|
||||||
{ ignorePublicAccess: true }
|
|
||||||
)
|
)
|
||||||
return Boolean(privilegeLevel)
|
return Boolean(privilegeLevel)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -681,7 +681,7 @@ async function _getProjects(
|
|||||||
const results = await Promise.all([
|
const results = await Promise.all([
|
||||||
ProjectGetter.promises.findAllUsersProjects(
|
ProjectGetter.promises.findAllUsersProjects(
|
||||||
userId,
|
userId,
|
||||||
'name lastUpdated lastUpdatedBy publicAccesLevel archived trashed owner_ref tokens compiler'
|
'name lastUpdated lastUpdatedBy publicAccesLevel archived trashed owner_ref tokens compiler quartoFlavor'
|
||||||
),
|
),
|
||||||
TagsHandler.promises.getAllTags(userId),
|
TagsHandler.promises.getAllTags(userId),
|
||||||
])
|
])
|
||||||
@@ -826,6 +826,7 @@ function _formatProjectInfo(project, accessLevel, source, userId) {
|
|||||||
archived,
|
archived,
|
||||||
trashed,
|
trashed,
|
||||||
compiler: project.compiler,
|
compiler: project.compiler,
|
||||||
|
quartoFlavor: project.quartoFlavor,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -880,6 +881,7 @@ async function _injectProjectUsers(projects) {
|
|||||||
: users[project.owner_ref.toString()],
|
: users[project.owner_ref.toString()],
|
||||||
owner_ref: undefined,
|
owner_ref: undefined,
|
||||||
compiler: project.compiler,
|
compiler: project.compiler,
|
||||||
|
quartoFlavor: project.quartoFlavor,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export const ProjectSchema = new Schema(
|
|||||||
version: { type: Number }, // incremented for every change in the project structure (folders and filenames)
|
version: { type: Number }, // incremented for every change in the project structure (folders and filenames)
|
||||||
publicAccesLevel: { type: String, default: 'private' },
|
publicAccesLevel: { type: String, default: 'private' },
|
||||||
compiler: { type: String, default: settings.defaultLatexCompiler },
|
compiler: { type: String, default: settings.defaultLatexCompiler },
|
||||||
|
quartoFlavor: { type: String, enum: ['revealjs', 'pdf'] },
|
||||||
spellCheckLanguage: { type: String, default: 'en' },
|
spellCheckLanguage: { type: String, default: 'en' },
|
||||||
deletedByExternalDataSource: { type: Boolean, default: false },
|
deletedByExternalDataSource: { type: Boolean, default: false },
|
||||||
description: { type: String, default: '' },
|
description: { type: String, default: '' },
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { usePermissionsContext } from '@/features/ide-react/context/permissions-
|
|||||||
import FileTreeActionButton from './file-tree-action-button'
|
import FileTreeActionButton from './file-tree-action-button'
|
||||||
import { useRailContext } from '../../ide-react/context/rail-context'
|
import { useRailContext } from '../../ide-react/context/rail-context'
|
||||||
import PythonRequirementsModal from './python-requirements-modal'
|
import PythonRequirementsModal from './python-requirements-modal'
|
||||||
|
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
|
||||||
|
|
||||||
export default function FileTreeActionButtons({
|
export default function FileTreeActionButtons({
|
||||||
fileTreeExpanded,
|
fileTreeExpanded,
|
||||||
@@ -19,6 +20,8 @@ export default function FileTreeActionButtons({
|
|||||||
const { write } = usePermissionsContext()
|
const { write } = usePermissionsContext()
|
||||||
const { handlePaneCollapse } = useRailContext()
|
const { handlePaneCollapse } = useRailContext()
|
||||||
const [showPythonModal, setShowPythonModal] = useState(false)
|
const [showPythonModal, setShowPythonModal] = useState(false)
|
||||||
|
const { compiler } = useProjectSettingsContext()
|
||||||
|
const isQuarto = compiler === 'quarto'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
canCreate,
|
canCreate,
|
||||||
@@ -112,7 +115,7 @@ export default function FileTreeActionButtons({
|
|||||||
iconType="delete"
|
iconType="delete"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{write && (
|
{write && isQuarto && (
|
||||||
<FileTreeActionButton
|
<FileTreeActionButton
|
||||||
id="python-packages"
|
id="python-packages"
|
||||||
description={t('python_packages')}
|
description={t('python_packages')}
|
||||||
|
|||||||
+12
-3
@@ -4,12 +4,21 @@ import { ProjectCompiler } from '../../../../../../../types/project-settings'
|
|||||||
// Map the stored compiler engine to the document format the project produces.
|
// Map the stored compiler engine to the document format the project produces.
|
||||||
// CLSI dispatches the real engine from the root file's extension, but the
|
// CLSI dispatches the real engine from the root file's extension, but the
|
||||||
// compiler field is a faithful, cheap proxy for the project's format.
|
// compiler field is a faithful, cheap proxy for the project's format.
|
||||||
function formatLabel(compiler: ProjectCompiler | undefined): {
|
function formatLabel(
|
||||||
|
compiler: ProjectCompiler | undefined,
|
||||||
|
quartoFlavor: 'revealjs' | 'pdf' | undefined
|
||||||
|
): {
|
||||||
label: string
|
label: string
|
||||||
variant: 'quarto' | 'typst' | 'latex'
|
variant: 'quarto-slides' | 'quarto' | 'typst' | 'latex'
|
||||||
} {
|
} {
|
||||||
switch (compiler) {
|
switch (compiler) {
|
||||||
case 'quarto':
|
case 'quarto':
|
||||||
|
if (quartoFlavor === 'revealjs') {
|
||||||
|
return { label: 'Quarto Slides', variant: 'quarto-slides' }
|
||||||
|
}
|
||||||
|
if (quartoFlavor === 'pdf') {
|
||||||
|
return { label: 'Quarto PDF', variant: 'quarto' }
|
||||||
|
}
|
||||||
return { label: 'Quarto', variant: 'quarto' }
|
return { label: 'Quarto', variant: 'quarto' }
|
||||||
case 'typst':
|
case 'typst':
|
||||||
return { label: 'Typst', variant: 'typst' }
|
return { label: 'Typst', variant: 'typst' }
|
||||||
@@ -24,7 +33,7 @@ type FormatCellProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function FormatCell({ project }: FormatCellProps) {
|
export default function FormatCell({ project }: FormatCellProps) {
|
||||||
const { label, variant } = formatLabel(project.compiler)
|
const { label, variant } = formatLabel(project.compiler, project.quartoFlavor)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -46,5 +46,6 @@ export const classHighlighter = tagHighlighter([
|
|||||||
{ tag: tags.invalid, class: 'tok-invalid' },
|
{ tag: tags.invalid, class: 'tok-invalid' },
|
||||||
{ tag: tags.punctuation, class: 'tok-punctuation' },
|
{ tag: tags.punctuation, class: 'tok-punctuation' },
|
||||||
// additional
|
// additional
|
||||||
|
{ tag: tags.attributeName, class: 'tok-attributeName' },
|
||||||
{ tag: tags.attributeValue, class: 'tok-attributeValue' },
|
{ tag: tags.attributeValue, class: 'tok-attributeValue' },
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -203,6 +203,9 @@ const staticTheme = EditorView.theme({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
fontWeight: 'normal',
|
fontWeight: 'normal',
|
||||||
},
|
},
|
||||||
|
// Bold and italic markup (e.g. *strong* _emphasis_ in Typst and Markdown)
|
||||||
|
'.tok-strong': { fontWeight: 'bold' },
|
||||||
|
'.tok-emphasis': { fontStyle: 'italic' },
|
||||||
'.cm-selectionLayer': {
|
'.cm-selectionLayer': {
|
||||||
zIndex: -10,
|
zIndex: -10,
|
||||||
},
|
},
|
||||||
|
|||||||
+38
-17
@@ -23,32 +23,53 @@ const LEVELS: NestingLevel[] = [
|
|||||||
// after it, so this stays clear of code.
|
// after it, so this stays clear of code.
|
||||||
const HEADING_REGEX = /^(=+)[ \t]+(.*\S)[ \t]*$/
|
const HEADING_REGEX = /^(=+)[ \t]+(.*\S)[ \t]*$/
|
||||||
|
|
||||||
|
// Count unescaped '$' signs on a line to track math-mode parity.
|
||||||
|
function countDollars(text: string): number {
|
||||||
|
let count = 0
|
||||||
|
for (let i = 0; i < text.length; i++) {
|
||||||
|
if (text[i] === '\\') { i++; continue }
|
||||||
|
if (text[i] === '$') count++
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
function computeOutline(
|
function computeOutline(
|
||||||
state: EditorState
|
state: EditorState
|
||||||
): ProjectionResult<FlatOutlineItem> {
|
): ProjectionResult<FlatOutlineItem> {
|
||||||
const items: FlatOutlineItem[] = []
|
const items: FlatOutlineItem[] = []
|
||||||
|
// Track whether we are inside a multi-line display math block.
|
||||||
|
// Each line with an odd number of unescaped '$' toggles the flag.
|
||||||
|
let inMath = false
|
||||||
|
|
||||||
for (let n = 1; n <= state.doc.lines; n++) {
|
for (let n = 1; n <= state.doc.lines; n++) {
|
||||||
const line = state.doc.line(n)
|
const line = state.doc.line(n)
|
||||||
const match = HEADING_REGEX.exec(line.text)
|
const text = line.text
|
||||||
if (!match) continue
|
|
||||||
|
|
||||||
const depth = match[1].length
|
// Only attempt heading detection when not inside a math block.
|
||||||
const level = LEVELS[Math.min(depth, LEVELS.length) - 1]
|
// (e.g. '= b+c$' on the second line of '$ a \n= b+c$' must be skipped.)
|
||||||
// Strip a trailing line comment, then a trailing label.
|
if (!inMath) {
|
||||||
const title = match[2]
|
const match = HEADING_REGEX.exec(text)
|
||||||
.replace(/\s*\/\/.*$/, '')
|
if (match) {
|
||||||
.replace(/\s*<[\w-]+>\s*$/, '')
|
const depth = match[1].length
|
||||||
.trim()
|
const level = LEVELS[Math.min(depth, LEVELS.length) - 1]
|
||||||
|
// Strip a trailing line comment, then a trailing label.
|
||||||
|
const title = match[2]
|
||||||
|
.replace(/\s*\/\/.*$/, '')
|
||||||
|
.replace(/\s*<[\w-]+>\s*$/, '')
|
||||||
|
.trim()
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
line: n,
|
line: n,
|
||||||
toLine: n,
|
toLine: n,
|
||||||
title,
|
title,
|
||||||
from: line.from,
|
from: line.from,
|
||||||
to: line.to,
|
to: line.to,
|
||||||
level,
|
level,
|
||||||
} as FlatOutlineItem)
|
} as FlatOutlineItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (countDollars(text) % 2 === 1) inMath = !inMath
|
||||||
}
|
}
|
||||||
|
|
||||||
return { items, status: ProjectionStatus.Complete }
|
return { items, status: ProjectionStatus.Complete }
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ import { typstDocumentOutline } from './document-outline'
|
|||||||
// Note on tree structure: rules starting with a lowercase letter in the grammar
|
// Note on tree structure: rules starting with a lowercase letter in the grammar
|
||||||
// are inline (no tree node), so their children are promoted to the parent.
|
// are inline (no tree node), so their children are promoted to the parent.
|
||||||
// E.g. codeArgItem, codeValue, callSuffix, codeArgList are all inline.
|
// E.g. codeArgItem, codeValue, callSuffix, codeArgList are all inline.
|
||||||
// Therefore:
|
// Named arg keys emit CodeArgKey (not CodeIdent) via codeIdentTokenizer,
|
||||||
// - The named-argument key "CodeIdent" is a *direct* child of CodeArgs.
|
// so CodeArgKey appears at the same level as other codeArgItem children.
|
||||||
// - Positional arguments that are identifiers are wrapped in CallExpr.
|
|
||||||
|
|
||||||
export const TypstLanguage = LRLanguage.define({
|
export const TypstLanguage = LRLanguage.define({
|
||||||
name: 'typst',
|
name: 'typst',
|
||||||
@@ -50,11 +49,13 @@ export const TypstLanguage = LRLanguage.define({
|
|||||||
CodeBool: t.atom,
|
CodeBool: t.atom,
|
||||||
|
|
||||||
// Identifiers:
|
// Identifiers:
|
||||||
// - direct child of CallExpr → function/method name
|
// CodeExpr/CodeIdent — bare #func (no args) → function style
|
||||||
// - direct child of CodeArgs → named argument key (key: value syntax)
|
// FuncExpr/CodeIdent — func call with args/method (#func(...), link.with(url)) → function style
|
||||||
// - everywhere else → plain variable
|
// CodeArgKey — named arg key (tokenizer pre-disambiguates on ':') → attributeName
|
||||||
'CallExpr/CodeIdent': t.function(t.variableName),
|
// CodeIdent — plain variable/constant reference (e.g. 'left', 'center') → variable
|
||||||
'CodeArgs/CodeIdent': t.attributeName,
|
'CodeExpr/CodeIdent': t.function(t.variableName),
|
||||||
|
'FuncExpr/CodeIdent': t.function(t.variableName),
|
||||||
|
CodeArgKey: t.attributeName,
|
||||||
CodeIdent: t.variableName,
|
CodeIdent: t.variableName,
|
||||||
|
|
||||||
// Literals in code mode
|
// Literals in code mode
|
||||||
@@ -73,8 +74,11 @@ export const TypstLanguage = LRLanguage.define({
|
|||||||
MathContent: t.string,
|
MathContent: t.string,
|
||||||
|
|
||||||
// Markup emphasis
|
// Markup emphasis
|
||||||
'Strong/"*" Strong/StrongText': t.strong,
|
'Strong/"*" Strong/StrongBody': t.strong,
|
||||||
'Emphasis/"_" Emphasis/EmphText': t.emphasis,
|
'Emphasis/"_" Emphasis/EmphBody': t.emphasis,
|
||||||
|
|
||||||
|
// Bare URLs (https://... / http://...)
|
||||||
|
URL: t.string,
|
||||||
|
|
||||||
// Labels (<name>) and references (@name)
|
// Labels (<name>) and references (@name)
|
||||||
'Label/"<" Label/">" Label/LabelName': t.labelName,
|
'Label/"<" Label/">" Label/LabelName': t.labelName,
|
||||||
@@ -97,6 +101,9 @@ const typstHighlightStyle = HighlightStyle.define([
|
|||||||
{ tag: t.heading, fontWeight: 'bold' },
|
{ tag: t.heading, fontWeight: 'bold' },
|
||||||
{ tag: t.strong, fontWeight: 'bold' },
|
{ tag: t.strong, fontWeight: 'bold' },
|
||||||
{ tag: t.emphasis, fontStyle: 'italic' },
|
{ tag: t.emphasis, fontStyle: 'italic' },
|
||||||
|
// Named arg keys (fill:, caption:, columns:…) — amber colour that reads
|
||||||
|
// well on both light and dark backgrounds, independent of theme CSS.
|
||||||
|
{ tag: t.attributeName, color: '#c47900' },
|
||||||
])
|
])
|
||||||
|
|
||||||
export const typst = () => {
|
export const typst = () => {
|
||||||
|
|||||||
@@ -8,22 +8,50 @@ import {
|
|||||||
RawBlockBody,
|
RawBlockBody,
|
||||||
RawBlockClose,
|
RawBlockClose,
|
||||||
RawInlineContent,
|
RawInlineContent,
|
||||||
CodeBlockBody,
|
|
||||||
BlockCommentBody,
|
BlockCommentBody,
|
||||||
LineCommentContent,
|
LineCommentContent,
|
||||||
MathContent,
|
MathContent,
|
||||||
|
CodeKeyword,
|
||||||
|
CodeIdent,
|
||||||
|
CodeArgKey,
|
||||||
|
StrongBody,
|
||||||
|
EmphBody,
|
||||||
} from './typst.terms.mjs'
|
} from './typst.terms.mjs'
|
||||||
|
|
||||||
const BACKTICK = 96 // `
|
const BACKTICK = 96 // `
|
||||||
const SLASH = 47 // /
|
const SLASH = 47 // /
|
||||||
const STAR = 42 // *
|
const STAR = 42 // *
|
||||||
const NEWLINE = 10 // \n
|
const NEWLINE = 10 // \n
|
||||||
const EQUALS = 61 // =
|
const EQUALS = 61 // =
|
||||||
const SPACE = 32 //
|
const SPACE = 32 //
|
||||||
const TAB = 9 // \t
|
const TAB = 9 // \t
|
||||||
const DOLLAR = 36 // $
|
const DOLLAR = 36 // $
|
||||||
const OPEN_BRACE = 123 // {
|
const OPEN_BRACE = 123 // {
|
||||||
const CLOSE_BRACE = 125 // }
|
const CLOSE_BRACE = 125 // }
|
||||||
|
const HASH = 35 // #
|
||||||
|
const UNDERSCORE = 95 // _
|
||||||
|
const DOT = 46 // .
|
||||||
|
const OPEN_PAREN = 40 // (
|
||||||
|
const COMMA = 44 // ,
|
||||||
|
const COLON = 58 // :
|
||||||
|
const SEMICOLON = 59 // ;
|
||||||
|
const OPEN_ANGLE = 60 // <
|
||||||
|
const CLOSE_ANGLE = 62 // >
|
||||||
|
const PLUS = 43 // +
|
||||||
|
|
||||||
|
const KEYWORDS = new Set([
|
||||||
|
'let', 'set', 'show', 'import', 'include',
|
||||||
|
'if', 'else', 'for', 'while', 'return',
|
||||||
|
'break', 'continue', 'in', 'as',
|
||||||
|
'and', 'or', 'not', 'context',
|
||||||
|
])
|
||||||
|
|
||||||
|
const BOOLS = new Set(['true', 'false', 'none', 'auto'])
|
||||||
|
|
||||||
|
const isAlpha = ch => (ch >= 65 && ch <= 90) || (ch >= 97 && ch <= 122)
|
||||||
|
const isDigit = ch => ch >= 48 && ch <= 57
|
||||||
|
const isIdentHead = ch => isAlpha(ch) || ch === UNDERSCORE
|
||||||
|
const isIdentTail = ch => isAlpha(ch) || isDigit(ch) || ch === UNDERSCORE || ch === 45
|
||||||
|
|
||||||
// ── headingTokenizer ────────────────────────────────────────────────────
|
// ── headingTokenizer ────────────────────────────────────────────────────
|
||||||
// Emits HeadingMark — the "=+" prefix plus the trailing whitespace.
|
// Emits HeadingMark — the "=+" prefix plus the trailing whitespace.
|
||||||
@@ -62,6 +90,17 @@ export const headingTitleTokenizer = new ExternalTokenizer(
|
|||||||
while (input.next !== -1 && input.next !== NEWLINE) {
|
while (input.next !== -1 && input.next !== NEWLINE) {
|
||||||
if (input.next === SLASH &&
|
if (input.next === SLASH &&
|
||||||
(input.peek(1) === SLASH || input.peek(1) === STAR)) break
|
(input.peek(1) === SLASH || input.peek(1) === STAR)) break
|
||||||
|
// Stop before a trailing '<label>' so it is parsed as a Label node
|
||||||
|
// rather than being merged into the heading title text.
|
||||||
|
// Only stops when '<' is immediately followed by a valid label name and '>'.
|
||||||
|
if (input.next === OPEN_ANGLE) {
|
||||||
|
const ch = input.peek(1)
|
||||||
|
if (isAlpha(ch) || isDigit(ch) || ch === UNDERSCORE) {
|
||||||
|
let j = 2
|
||||||
|
while (isIdentTail(input.peek(j)) || input.peek(j) === DOT || input.peek(j) === COLON) j++
|
||||||
|
if (input.peek(j) === CLOSE_ANGLE) break
|
||||||
|
}
|
||||||
|
}
|
||||||
input.advance()
|
input.advance()
|
||||||
hasContent = true
|
hasContent = true
|
||||||
}
|
}
|
||||||
@@ -105,6 +144,20 @@ export const rawTokenizer = new ExternalTokenizer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stack.canShift(RawBlockBody)) {
|
if (stack.canShift(RawBlockBody)) {
|
||||||
|
// Guard: must genuinely follow a RawBlockOpen (which ends with \n).
|
||||||
|
// Walk backward past any lang tag (A-Za-z0-9) and require ```.
|
||||||
|
// This blocks spurious LALR-merged states from consuming body text.
|
||||||
|
if (input.peek(-1) !== NEWLINE) return
|
||||||
|
let back = -2
|
||||||
|
while (
|
||||||
|
(input.peek(back) >= 65 && input.peek(back) <= 90) ||
|
||||||
|
(input.peek(back) >= 97 && input.peek(back) <= 122) ||
|
||||||
|
(input.peek(back) >= 48 && input.peek(back) <= 57)
|
||||||
|
) { back-- }
|
||||||
|
if (input.peek(back) !== BACKTICK ||
|
||||||
|
input.peek(back - 1) !== BACKTICK ||
|
||||||
|
input.peek(back - 2) !== BACKTICK) return
|
||||||
|
|
||||||
let hasContent = false
|
let hasContent = false
|
||||||
while (input.next !== -1) {
|
while (input.next !== -1) {
|
||||||
if (
|
if (
|
||||||
@@ -136,36 +189,6 @@ export const rawInlineTokenizer = new ExternalTokenizer(
|
|||||||
{ contextual: false }
|
{ contextual: false }
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── codeBlockTokenizer ──────────────────────────────────────────────────
|
|
||||||
// Emits CodeBlockBody — the interior of a #{ ... } code block.
|
|
||||||
// Tracks brace nesting depth so that inner braces (e.g. #{ f({ x }) })
|
|
||||||
// are included in the body rather than closing the outer block.
|
|
||||||
export const codeBlockTokenizer = new ExternalTokenizer(
|
|
||||||
(input, _stack) => {
|
|
||||||
// The opening '{' has already been consumed by the grammar rule.
|
|
||||||
let depth = 1
|
|
||||||
let hasContent = false
|
|
||||||
while (input.next !== -1) {
|
|
||||||
const ch = input.next
|
|
||||||
if (ch === OPEN_BRACE) {
|
|
||||||
depth++
|
|
||||||
input.advance()
|
|
||||||
hasContent = true
|
|
||||||
} else if (ch === CLOSE_BRACE) {
|
|
||||||
if (depth === 1) break // leave this '}' for the grammar rule
|
|
||||||
depth--
|
|
||||||
input.advance()
|
|
||||||
hasContent = true
|
|
||||||
} else {
|
|
||||||
input.advance()
|
|
||||||
hasContent = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (hasContent) input.acceptToken(CodeBlockBody)
|
|
||||||
},
|
|
||||||
{ contextual: false }
|
|
||||||
)
|
|
||||||
|
|
||||||
// ── blockCommentTokenizer ───────────────────────────────────────────────
|
// ── blockCommentTokenizer ───────────────────────────────────────────────
|
||||||
// Emits BlockCommentBody — the interior of a /* ... */ comment.
|
// Emits BlockCommentBody — the interior of a /* ... */ comment.
|
||||||
// Typst supports nested block comments (/* /* inner */ outer */), so this
|
// Typst supports nested block comments (/* /* inner */ outer */), so this
|
||||||
@@ -215,9 +238,13 @@ export const lineCommentContentTokenizer = new ExternalTokenizer(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ── mathContentTokenizer ────────────────────────────────────────────────
|
// ── mathContentTokenizer ────────────────────────────────────────────────
|
||||||
// Emits MathContent — everything between the $...$ delimiters (no newlines).
|
// Emits MathContent — one line of content between the $...$ delimiters.
|
||||||
// External rather than a @tokens rule for the same reason as LineCommentContent:
|
// Stops at '$' or '\n' so each token is bounded to a single line.
|
||||||
// ![$\n]+ overlaps with spaces, '<', '@', and other literals in merged states.
|
//
|
||||||
|
// The grammar uses MathContent* (not MathContent?) so multi-line display
|
||||||
|
// math ($ ... \n ... $) is handled by multiple MathContent tokens, one per
|
||||||
|
// line, with @skip consuming the newlines in between. This keeps each
|
||||||
|
// token short and prevents a stray '$' from consuming the whole document.
|
||||||
export const mathContentTokenizer = new ExternalTokenizer(
|
export const mathContentTokenizer = new ExternalTokenizer(
|
||||||
(input, _stack) => {
|
(input, _stack) => {
|
||||||
let hasContent = false
|
let hasContent = false
|
||||||
@@ -229,3 +256,174 @@ export const mathContentTokenizer = new ExternalTokenizer(
|
|||||||
},
|
},
|
||||||
{ contextual: false }
|
{ contextual: false }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ── codeKeywordTokenizer ─────────────────────────────────────────────────
|
||||||
|
// Emits CodeKeyword (let, set, for, while, in, …) ONLY when the preceding
|
||||||
|
// character is '#', i.e. we are immediately after the '#' sigil in a CodeExpr.
|
||||||
|
//
|
||||||
|
// The peek(-1)==='#' guard is what prevents LALR state-merging from causing
|
||||||
|
// these tokens to fire in body-text positions. Common English words like
|
||||||
|
// "in", "for", "while", "return" appear in markup paragraphs; without the
|
||||||
|
// guard they would be highlighted as keywords due to LALR-merged states where
|
||||||
|
// CodeKeyword is technically in the valid set.
|
||||||
|
export const codeKeywordTokenizer = new ExternalTokenizer(
|
||||||
|
(input, stack) => {
|
||||||
|
if (!stack.canShift(CodeKeyword)) return
|
||||||
|
// Valid positions: after '#', ':', '{' (code block start), or ';'.
|
||||||
|
// Walk back past optional whitespace.
|
||||||
|
let back = -1
|
||||||
|
while (input.peek(back) === SPACE || input.peek(back) === TAB || input.peek(back) === NEWLINE) back--
|
||||||
|
const kwPrev = input.peek(back)
|
||||||
|
if (kwPrev !== HASH && kwPrev !== COLON && kwPrev !== OPEN_BRACE && kwPrev !== SEMICOLON) return
|
||||||
|
|
||||||
|
// Peek ahead to read the full identifier without advancing.
|
||||||
|
let len = 0
|
||||||
|
while (true) {
|
||||||
|
const ch = input.peek(len)
|
||||||
|
if (isIdentHead(ch) || (len > 0 && isIdentTail(ch))) { len++ } else { break }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len === 0) return
|
||||||
|
|
||||||
|
const chars = []
|
||||||
|
for (let i = 0; i < len; i++) chars.push(input.peek(i))
|
||||||
|
const word = String.fromCharCode(...chars)
|
||||||
|
|
||||||
|
if (!KEYWORDS.has(word)) return
|
||||||
|
|
||||||
|
for (let i = 0; i < len; i++) input.advance()
|
||||||
|
input.acceptToken(CodeKeyword)
|
||||||
|
},
|
||||||
|
{ contextual: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── codeIdentTokenizer ───────────────────────────────────────────────────
|
||||||
|
// Emits CodeIdent — identifier tokens inside code expressions (#ident,
|
||||||
|
// #func(args), #obj.method, etc.).
|
||||||
|
//
|
||||||
|
// Moving CodeIdent from @tokens to an external tokenizer allows a
|
||||||
|
// character-level guard: we only emit when the preceding non-whitespace
|
||||||
|
// character is one of '#', '.', '(', ',' — genuine code-context positions.
|
||||||
|
// This stops the token from firing in markup body text where LALR-merged
|
||||||
|
// states would otherwise cause '_italic_' to be consumed as one big
|
||||||
|
// CodeIdent (since '_' is a valid identHead) instead of opening Emphasis.
|
||||||
|
//
|
||||||
|
// Keywords and bools are excluded so codeKeywordTokenizer / CodeBool can
|
||||||
|
// handle them without conflict.
|
||||||
|
//
|
||||||
|
// The backward scan runs BEFORE any canShift gate. canShift(CodeArgKey) is
|
||||||
|
// unreliable (LALR state merging can suppress it even at genuine arg-key
|
||||||
|
// positions, e.g. 'caption:' after a complex nested call like 'table(...)').
|
||||||
|
// We derive couldBeArgKey from character-level evidence ('(' or ',') and use
|
||||||
|
// that to decide whether to continue even when canShift(CodeIdent) is false.
|
||||||
|
export const codeIdentTokenizer = new ExternalTokenizer(
|
||||||
|
(input, stack) => {
|
||||||
|
const couldBeIdent = stack.canShift(CodeIdent)
|
||||||
|
|
||||||
|
// Walk back past whitespace — primary context discriminator.
|
||||||
|
let back = -1
|
||||||
|
while (input.peek(back) === SPACE || input.peek(back) === TAB || input.peek(back) === NEWLINE) back--
|
||||||
|
const prev = input.peek(back)
|
||||||
|
|
||||||
|
if (prev !== HASH && prev !== DOT && prev !== OPEN_PAREN && prev !== COMMA && prev !== EQUALS && prev !== COLON && prev !== PLUS) {
|
||||||
|
if (!isIdentTail(prev)) {
|
||||||
|
// prev is a structural delimiter (e.g. ')' after a function call, '{' at
|
||||||
|
// block start, '}' after a nested block). These are valid statement-start
|
||||||
|
// positions inside a CodeBlock's codeStatement* list. Trust canShift —
|
||||||
|
// it's reliable in the grammar-parsed code-block states.
|
||||||
|
if (!couldBeIdent) return
|
||||||
|
} else {
|
||||||
|
// prev looks like the tail of a preceding word — scan back to find '#' or ':'.
|
||||||
|
// Accepting ':' lets multi-word chains like 'show sel: set text' work.
|
||||||
|
let b = back
|
||||||
|
while (isIdentTail(input.peek(b))) b--
|
||||||
|
while (input.peek(b) === SPACE || input.peek(b) === TAB || input.peek(b) === NEWLINE) b--
|
||||||
|
const chainEnd = input.peek(b)
|
||||||
|
if (chainEnd !== HASH && chainEnd !== COLON) {
|
||||||
|
// Could be second+ statement in a code block (e.g. after 'let x = 1').
|
||||||
|
if (!couldBeIdent) return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In arg-delimiter positions ('(' or ',') we may emit CodeArgKey regardless
|
||||||
|
// of canShift(CodeIdent) — LALR merging can suppress canShift(CodeIdent)
|
||||||
|
// after a complex first argument (e.g. figure(table(...), caption: ...)).
|
||||||
|
// ':' and '=' are value positions, NOT arg-key positions.
|
||||||
|
const couldBeArgKey = prev === OPEN_PAREN || prev === COMMA
|
||||||
|
if (!couldBeIdent && !couldBeArgKey) return
|
||||||
|
|
||||||
|
// Must start with an identifier head character.
|
||||||
|
if (!isIdentHead(input.next)) return
|
||||||
|
|
||||||
|
// Peek ahead to read the full identifier.
|
||||||
|
let len = 0
|
||||||
|
while (true) {
|
||||||
|
const ch = input.peek(len)
|
||||||
|
if (len === 0 ? isIdentHead(ch) : isIdentTail(ch)) { len++ } else { break }
|
||||||
|
}
|
||||||
|
if (len === 0) return
|
||||||
|
|
||||||
|
const chars = []
|
||||||
|
for (let i = 0; i < len; i++) chars.push(input.peek(i))
|
||||||
|
const word = String.fromCharCode(...chars)
|
||||||
|
|
||||||
|
// Let codeKeywordTokenizer handle keywords; let CodeBool handle bools.
|
||||||
|
if (KEYWORDS.has(word) || BOOLS.has(word)) return
|
||||||
|
|
||||||
|
// Emit CodeArgKey when this identifier is immediately followed by ':'.
|
||||||
|
// Only applies in arg-delimiter positions (couldBeArgKey).
|
||||||
|
let isArgKey = false
|
||||||
|
if (couldBeArgKey) {
|
||||||
|
let afterLen = len
|
||||||
|
while (input.peek(afterLen) === SPACE || input.peek(afterLen) === TAB) afterLen++
|
||||||
|
isArgKey = (input.peek(afterLen) === COLON)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < len; i++) input.advance()
|
||||||
|
if (isArgKey) {
|
||||||
|
input.acceptToken(CodeArgKey)
|
||||||
|
} else if (couldBeIdent) {
|
||||||
|
input.acceptToken(CodeIdent)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ contextual: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── strongBodyTokenizer ──────────────────────────────────────────────────
|
||||||
|
// Emits StrongBody — the content between the '*' delimiters of a Strong node.
|
||||||
|
//
|
||||||
|
// contextual: true — only fires when StrongBody is in the valid set, i.e.
|
||||||
|
// inside Strong → "*" . StrongBody? "*". This state is very specific and
|
||||||
|
// is not merged with item* by Lezer's aggressive LALR merging, so canShift
|
||||||
|
// is a reliable guard here.
|
||||||
|
//
|
||||||
|
// Reads everything up to the first '*' or newline (Typst bold does not span
|
||||||
|
// lines). A trailing '*' that is the closing delimiter is left for the
|
||||||
|
// grammar rule to consume.
|
||||||
|
export const strongBodyTokenizer = new ExternalTokenizer(
|
||||||
|
(input, _stack) => {
|
||||||
|
let hasContent = false
|
||||||
|
while (input.next !== -1 && input.next !== STAR && input.next !== NEWLINE) {
|
||||||
|
input.advance()
|
||||||
|
hasContent = true
|
||||||
|
}
|
||||||
|
if (hasContent) input.acceptToken(StrongBody)
|
||||||
|
},
|
||||||
|
{ contextual: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── emphBodyTokenizer ────────────────────────────────────────────────────
|
||||||
|
// Emits EmphBody — the content between the '_' delimiters of an Emphasis node.
|
||||||
|
// Same design as strongBodyTokenizer; stops at '_' or newline.
|
||||||
|
export const emphBodyTokenizer = new ExternalTokenizer(
|
||||||
|
(input, _stack) => {
|
||||||
|
let hasContent = false
|
||||||
|
while (input.next !== -1 && input.next !== UNDERSCORE && input.next !== NEWLINE) {
|
||||||
|
input.advance()
|
||||||
|
hasContent = true
|
||||||
|
}
|
||||||
|
if (hasContent) input.acceptToken(EmphBody)
|
||||||
|
},
|
||||||
|
{ contextual: true }
|
||||||
|
)
|
||||||
|
|||||||
@@ -5,8 +5,10 @@
|
|||||||
// headingTitleTokenizer — HeadingTitle: the title text to end of line
|
// headingTitleTokenizer — HeadingTitle: the title text to end of line
|
||||||
// rawTokenizer — triple-backtick raw block open/body/close
|
// rawTokenizer — triple-backtick raw block open/body/close
|
||||||
// rawInlineTokenizer — single-backtick raw inline content
|
// rawInlineTokenizer — single-backtick raw inline content
|
||||||
// codeBlockTokenizer — brace-depth tracking inside #{ ... }
|
|
||||||
// blockCommentTokenizer — depth-tracked nested /* ... */ comments
|
// blockCommentTokenizer — depth-tracked nested /* ... */ comments
|
||||||
|
// codeIdentTokenizer — CodeIdent: identifier, only fires in code context
|
||||||
|
// strongBodyTokenizer — StrongBody: content inside *...*
|
||||||
|
// emphBodyTokenizer — EmphBody: content inside _..._
|
||||||
|
|
||||||
@top Document { item* }
|
@top Document { item* }
|
||||||
|
|
||||||
@@ -24,8 +26,9 @@ item {
|
|||||||
Label |
|
Label |
|
||||||
Ref |
|
Ref |
|
||||||
Escape |
|
Escape |
|
||||||
Newline |
|
URL |
|
||||||
MarkupContent
|
MarkupContent |
|
||||||
|
ClosingSquare
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Headings ──────────────────────────────────────────────────────────────
|
// ── Headings ──────────────────────────────────────────────────────────────
|
||||||
@@ -58,63 +61,140 @@ RawInline { "`" RawInlineContent? "`" }
|
|||||||
// #[ ... ] — content block (re-parses as markup items)
|
// #[ ... ] — content block (re-parses as markup items)
|
||||||
CodeExpr { "#" codeExprBody }
|
CodeExpr { "#" codeExprBody }
|
||||||
|
|
||||||
|
// codeExprBody: forms valid after '#' in markup, or after ':' / '=' in a
|
||||||
|
// keyword-body. FuncExpr handles ident+callSuffix(s); bare CodeIdent handles
|
||||||
|
// a plain variable reference (#x). No CallExpr with callSuffix* here — that
|
||||||
|
// *-quantifier makes both shift and reduce carry !call precedence (a tie that
|
||||||
|
// @right cannot resolve reliably once codeStatement* state-merging is in play).
|
||||||
codeExprBody {
|
codeExprBody {
|
||||||
KeywordExpr |
|
KeywordExpr |
|
||||||
AtomExpr |
|
AtomExpr |
|
||||||
CallExpr |
|
FuncExpr |
|
||||||
|
CodeIdent |
|
||||||
CodeBlock |
|
CodeBlock |
|
||||||
ContentBlock
|
ContentBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
// CallExpr? covers '#set text(size: 12pt)', '#show heading: ...', etc.
|
// callOrValue covers the subject of a keyword expression (#set text, #show link,
|
||||||
// The optional CallExpr is only shifted when the next token is CodeIdent,
|
// #import "pkg", #let name). keywordBody is exclusive: ':' for show-rule bodies
|
||||||
// so there is no shift/reduce conflict with other items that follow keywords.
|
// and '=' for let-binding values (a keyword expression never has both).
|
||||||
KeywordExpr { CodeKeyword CallExpr? }
|
// Two precedences:
|
||||||
|
// call @right — prefer extending callSuffixes (FuncExpr) over completing the
|
||||||
|
// FuncExpr and letting '(' start a new statement. The `!call` marker
|
||||||
|
// encodes the shift as (call << 2) and the FuncExpr reduce as
|
||||||
|
// (call << 2) - 1 (due to @right); shift > reduce, so callSuffix
|
||||||
|
// chains are greedily extended. Without @right both actions have
|
||||||
|
// the same numeric precedence and the conflict is unresolved.
|
||||||
|
// kw — prefer CodeKeyword !kw callOrValueAndBody over CodeKeyword keywordBody?
|
||||||
|
// when an identifier follows the keyword. shift = kw << 2, reduce
|
||||||
|
// (second alternative) = 0; kw > 0, no @right needed.
|
||||||
|
// add — resolves the shift/reduce conflict when a '+' follows a codeArgValue:
|
||||||
|
// SHIFT '+' (extend codeArgValue → codeArgValue !add "+" codeValue): prec add
|
||||||
|
// REDUCE codeArgItem → codeArgValue (complete arg): prec 0
|
||||||
|
// add > 0 → shift wins, so 0.8pt + brand stays as one arg value.
|
||||||
|
@precedence { call @right, kw, add }
|
||||||
|
|
||||||
|
// KeywordExpr: used in markup-level code (#show, #let, #set …) AND nested
|
||||||
|
// inside codeExprBody (e.g. the RHS after ':' in a show-rule).
|
||||||
|
// Same two-alternative structure as codeStatement: the !kw on the first
|
||||||
|
// alternative gives the shift prec kw > 0 over the unannotated reduce of the
|
||||||
|
// second alternative (prec 0). This avoids the call-vs-call tie that arises
|
||||||
|
// from the old `callOrValue?` optional pattern.
|
||||||
|
KeywordExpr {
|
||||||
|
CodeKeyword !kw callOrValueAndBody |
|
||||||
|
CodeKeyword keywordBody?
|
||||||
|
}
|
||||||
|
|
||||||
|
// callOrValue: FuncExpr for "ident(args)" / "ident.method", bare CodeIdent for
|
||||||
|
// a plain name, CodeString for string subjects like #import "pkg".
|
||||||
|
// FuncExpr requires at least one callSuffix, so at [CodeIdent ·] seeing '(':
|
||||||
|
// SHIFT (start callSuffixes, prec call) vs REDUCE bare CodeIdent (prec 0).
|
||||||
|
// call > 0 → shift wins cleanly.
|
||||||
|
callOrValue { FuncExpr | CodeIdent | CodeString }
|
||||||
|
keywordBody { ":" codeExprBody | "=" codeValue }
|
||||||
AtomExpr { CodeBool }
|
AtomExpr { CodeBool }
|
||||||
|
|
||||||
CallExpr { CodeIdent callSuffix* }
|
// codeStatement is the unit inside a CodeBlock's brace body.
|
||||||
|
// Two explicit alternatives for the keyword case avoid the LALR ambiguity
|
||||||
|
// that arises from codeStatement* merging when callOrValue? is optional.
|
||||||
|
// The !kw annotation on the first alternative (shift callOrValueAndBody) has
|
||||||
|
// higher precedence than the bare reduce of the second alternative (prec 0),
|
||||||
|
// so 'show strong: …' grabs 'strong' as callOrValue rather than completing
|
||||||
|
// KeywordExpr early with empty callOrValue.
|
||||||
|
codeStatement {
|
||||||
|
CodeKeyword !kw callOrValueAndBody |
|
||||||
|
CodeKeyword keywordBody? |
|
||||||
|
codeValue |
|
||||||
|
";"
|
||||||
|
}
|
||||||
|
callOrValueAndBody { callOrValue keywordBody? }
|
||||||
|
|
||||||
|
// FuncExpr: identifier followed by one-or-more call suffixes.
|
||||||
|
// callSuffixes uses explicit left-recursion (not +) so the !call annotation
|
||||||
|
// on the recursive extension point gives the shift prec call vs the unannotated
|
||||||
|
// reduce of codeValue → FuncExpr (prec 0) — shift wins, no @right tie.
|
||||||
|
callSuffixes { callSuffix | callSuffixes !call callSuffix }
|
||||||
|
FuncExpr { CodeIdent !call callSuffixes }
|
||||||
callSuffix {
|
callSuffix {
|
||||||
CodeArgs |
|
CodeArgs |
|
||||||
"." CodeIdent
|
"." CodeIdent |
|
||||||
|
ContentBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
CodeArgs { "(" codeArgList? ")" }
|
CodeArgs { "(" codeArgList? ")" }
|
||||||
codeArgList { codeArgItem ("," codeArgItem)* ","? }
|
codeArgList { codeArgItem ("," codeArgItem)* ","? }
|
||||||
codeArgItem {
|
codeArgItem {
|
||||||
CodeIdent ":" codeValue |
|
CodeArgKey ":" codeArgValue |
|
||||||
codeValue
|
codeArgValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// codeArgValue extends codeValue with '+' chaining for expressions like
|
||||||
|
// `stroke: 0.8pt + brand` or `fill: base + overlay`.
|
||||||
|
// Left-recursive rule: LALR state for codeArgValue · seeing '+':
|
||||||
|
// SHIFT '+' (extend, !add prec): prec add > 0
|
||||||
|
// REDUCE codeArgItem → codeArgValue (complete): prec 0
|
||||||
|
// add > 0 → shift wins cleanly. No @right needed (strict dominance).
|
||||||
|
// Only used inside CodeArgs, so codeStatement* LALR-merging does not apply.
|
||||||
|
codeArgValue { codeValue | codeArgValue !add "+" codeValue }
|
||||||
|
|
||||||
codeValue {
|
codeValue {
|
||||||
CodeString |
|
CodeString |
|
||||||
CodeNumber |
|
CodeNumber |
|
||||||
CodeBool |
|
CodeBool |
|
||||||
CallExpr |
|
FuncExpr |
|
||||||
|
CodeIdent |
|
||||||
ContentBlock |
|
ContentBlock |
|
||||||
CodeBlock |
|
CodeBlock |
|
||||||
InlineMath
|
InlineMath |
|
||||||
|
CodeArray
|
||||||
}
|
}
|
||||||
|
|
||||||
// CodeBlockBody depth-tracks braces so #{ let x = { 1 } } parses correctly.
|
// Typst array / tuple / dictionary literal: (a, b) or (key: val, …)
|
||||||
CodeBlock { "{" CodeBlockBody? "}" }
|
// Reuses codeArgList so named-key entries like (auto, 1fr) work too.
|
||||||
|
CodeArray { "(" codeArgList? ")" }
|
||||||
|
|
||||||
|
// CodeBlock parses its content as a codeStatement* list so that keywords
|
||||||
|
// (show, let, set…) and identifiers inside braces receive proper highlighting.
|
||||||
|
CodeBlock { "{" codeStatement* "}" }
|
||||||
// ContentBlock re-enters markup mode, allowing #[*bold* text].
|
// ContentBlock re-enters markup mode, allowing #[*bold* text].
|
||||||
ContentBlock { "[" item* "]" }
|
ContentBlock { "[" item* "]" }
|
||||||
|
|
||||||
// ── Math ──────────────────────────────────────────────────────────────────
|
// ── Math ──────────────────────────────────────────────────────────────────
|
||||||
// Both inline ($x^2$) and display ($ x^2 $) math use the same node type.
|
// Both inline ($x^2$) and display ($ x^2 $) math use the same node type.
|
||||||
InlineMath { "$" MathContent? "$" }
|
// MathContent* (not ?) allows multi-line display math: each line becomes one
|
||||||
|
// MathContent token (stopping at '\n'), and @skip consumes the newlines between.
|
||||||
|
InlineMath { "$" MathContent* "$" }
|
||||||
|
|
||||||
// ── Markup formatting ─────────────────────────────────────────────────────
|
// ── Markup formatting ─────────────────────────────────────────────────────
|
||||||
// Cross-nesting of Strong/Emphasis is intentionally excluded to avoid a
|
// Strong and Emphasis use flat external body tokens (StrongBody / EmphBody)
|
||||||
// mutual-recursion cycle (Strong→Emphasis→Strong) that causes state explosion
|
// rather than recursive strongItem* / emphItem* loops. The loop approach
|
||||||
// in the Lezer LR automaton builder. StrongText includes '_' and EmphText
|
// triggered LALR state merging that caused item*-level tokens (MarkupContent,
|
||||||
// includes '*', so the nested delimiters are treated as plain text inside the
|
// CodeIdent) to win over StrongText/EmphText inside the construct, so the
|
||||||
// opposite construct rather than producing error nodes.
|
// body nodes were never produced. The flat external tokens are contextual
|
||||||
Strong { "*" strongItem* "*" }
|
// (canShift only fires inside Strong/Emphasis) and reliably avoid those
|
||||||
strongItem { CodeExpr | InlineMath | RawInline | Label | Ref | StrongText }
|
// merged states.
|
||||||
|
Strong { "*" StrongBody? "*" }
|
||||||
Emphasis { "_" emphItem* "_" }
|
Emphasis { "_" EmphBody? "_" }
|
||||||
emphItem { CodeExpr | InlineMath | RawInline | Label | Ref | EmphText }
|
|
||||||
|
|
||||||
// ── Labels and references ─────────────────────────────────────────────────
|
// ── Labels and references ─────────────────────────────────────────────────
|
||||||
Label { "<" LabelName ">" }
|
Label { "<" LabelName ">" }
|
||||||
@@ -142,10 +222,6 @@ Escape { "\\" EscapeChar }
|
|||||||
RawInlineContent
|
RawInlineContent
|
||||||
}
|
}
|
||||||
|
|
||||||
@external tokens codeBlockTokenizer from "./tokens.mjs" {
|
|
||||||
CodeBlockBody
|
|
||||||
}
|
|
||||||
|
|
||||||
@external tokens blockCommentTokenizer from "./tokens.mjs" {
|
@external tokens blockCommentTokenizer from "./tokens.mjs" {
|
||||||
BlockCommentBody
|
BlockCommentBody
|
||||||
}
|
}
|
||||||
@@ -158,30 +234,44 @@ Escape { "\\" EscapeChar }
|
|||||||
MathContent
|
MathContent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@external tokens codeKeywordTokenizer from "./tokens.mjs" {
|
||||||
|
CodeKeyword
|
||||||
|
}
|
||||||
|
|
||||||
|
// CodeIdent is external so codeIdentTokenizer can apply a character-level
|
||||||
|
// guard: it only emits when the preceding non-whitespace character is one of
|
||||||
|
// '#', '.', '(', ',' — i.e. genuinely inside a code expression. This stops
|
||||||
|
// the token from firing in markup body text, where LALR state merging would
|
||||||
|
// otherwise cause the entire token (including any leading '_') to be consumed
|
||||||
|
// as a code identifier instead of letting '_' open an Emphasis.
|
||||||
|
// CodeArgKey is emitted by the same tokenizer when an identifier is immediately
|
||||||
|
// followed by ':' — the tokenizer pre-disambiguates named arg keys so the LALR
|
||||||
|
// parser does not need to choose between codeArgItem alternatives on lookahead.
|
||||||
|
@external tokens codeIdentTokenizer from "./tokens.mjs" {
|
||||||
|
CodeIdent,
|
||||||
|
CodeArgKey
|
||||||
|
}
|
||||||
|
|
||||||
|
@external tokens strongBodyTokenizer from "./tokens.mjs" {
|
||||||
|
StrongBody
|
||||||
|
}
|
||||||
|
|
||||||
|
@external tokens emphBodyTokenizer from "./tokens.mjs" {
|
||||||
|
EmphBody
|
||||||
|
}
|
||||||
|
|
||||||
// ── Regular tokens ────────────────────────────────────────────────────────
|
// ── Regular tokens ────────────────────────────────────────────────────────
|
||||||
@tokens {
|
@tokens {
|
||||||
// Horizontal whitespace only. Newlines are kept as explicit Newline items
|
// All whitespace including newlines. Heading detection still works because
|
||||||
// so that HeadingMark (which checks start-of-line via input.peek(-1)) can
|
// headingTokenizer uses input.peek(-1) on the raw character stream — it sees
|
||||||
// reliably detect newlines in the raw input stream.
|
// the '\n' byte regardless of what @skip consumes at the token level.
|
||||||
spaces { $[ \t]+ }
|
// Including '\n' here lets multi-line code expressions (e.g. #figure(\n ...\n))
|
||||||
|
// parse without error instead of triggering Lezer error recovery.
|
||||||
// Keywords take precedence over identifiers when they match fully
|
spaces { $[ \t\n\r]+ }
|
||||||
// (e.g. "let" → CodeKeyword, "letter" → CodeIdent).
|
|
||||||
CodeKeyword {
|
|
||||||
"let" | "set" | "show" | "import" | "include" |
|
|
||||||
"if" | "else" | "for" | "while" | "return" |
|
|
||||||
"break" | "continue" | "in" | "as" |
|
|
||||||
"and" | "or" | "not" | "context"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Boolean / null literals — distinct from keywords for highlighting.
|
// Boolean / null literals — distinct from keywords for highlighting.
|
||||||
CodeBool { "true" | "false" | "none" | "auto" }
|
CodeBool { "true" | "false" | "none" | "auto" }
|
||||||
|
|
||||||
// General identifier: [A-Za-z_][A-Za-z0-9_-]*
|
|
||||||
CodeIdent { identHead identTail* }
|
|
||||||
identHead { @asciiLetter | "_" }
|
|
||||||
identTail { @asciiLetter | @digit | "_" | "-" }
|
|
||||||
|
|
||||||
// Double-quoted string with backslash escapes (no single-quoted strings in Typst).
|
// Double-quoted string with backslash escapes (no single-quoted strings in Typst).
|
||||||
CodeString { '"' (!["\\\n] | "\\" _)* '"' }
|
CodeString { '"' (!["\\\n] | "\\" _)* '"' }
|
||||||
|
|
||||||
@@ -191,41 +281,42 @@ Escape { "\\" EscapeChar }
|
|||||||
("pt" | "mm" | "cm" | "in" | "em" | "rem" | "fr" | "deg" | "rad" | "%")?
|
("pt" | "mm" | "cm" | "in" | "em" | "rem" | "fr" | "deg" | "rad" | "%")?
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text tokens for markup contexts; each excludes its own delimiters.
|
// URL: bare https:// or http:// links in markup text. Matched as a single
|
||||||
// HeadingText, LineCommentContent, and MathContent are external tokens
|
// token so '://' is never split into ':' + LineComment '//…'. Stops at
|
||||||
// (see above) — broad "read-to-delimiter" tokens that would otherwise
|
// whitespace and angle brackets (labels use '<…>').
|
||||||
// conflict with every other literal token in LALR-merged states.
|
URL { ("https" | "http") "://" (![ \t\n<>])* }
|
||||||
// '<' is excluded from StrongText/EmphText so that Label ('<' LabelName '>')
|
|
||||||
// is recognised inside strong/emphasis rather than consumed as plain text.
|
|
||||||
StrongText { ![\n*$#`<@\\]+ }
|
|
||||||
EmphText { ![\n_$#`<@\\]+ }
|
|
||||||
|
|
||||||
// Regular markup: excludes all special-character starters plus whitespace
|
// Regular markup: excludes all special-character starters plus whitespace
|
||||||
// (whitespace is handled by @skip). The '/' is excluded so that '//' and
|
// (whitespace is handled by @skip). The '/' is excluded so that '//' and
|
||||||
// '/*' are not accidentally consumed as plain text.
|
// '/*' are not accidentally consumed as plain text. ']' is excluded so
|
||||||
MarkupContent { ![\n \t=*_$#/<@`\\]+ }
|
// that ContentBlock { "[" item* "]" } can always close reliably — a bare
|
||||||
|
// ']' in body text is matched as ClosingSquare instead.
|
||||||
|
MarkupContent { ![\n \t\]=*_$#/<@`\\]+ }
|
||||||
|
|
||||||
|
// Fallback for a bare ']' in markup text (outside any ContentBlock).
|
||||||
|
// Inside ContentBlock the literal "]" terminal wins via @precedence.
|
||||||
|
ClosingSquare { "]" }
|
||||||
|
|
||||||
// Label names: identifiers with optional dots/colons (e.g. <sec:intro>).
|
// Label names: identifiers with optional dots/colons (e.g. <sec:intro>).
|
||||||
LabelName { (identHead | @digit) (identTail | "." | ":")* }
|
LabelName { (@asciiLetter | "_" | @digit) (@asciiLetter | @digit | "_" | "-" | "." | ":")* }
|
||||||
RefName { identHead identTail* }
|
RefName { (@asciiLetter | "_") (@asciiLetter | @digit | "_" | "-")* }
|
||||||
|
|
||||||
// Escape: any single character after backslash.
|
// Escape: any single character after backslash.
|
||||||
EscapeChar { _ }
|
EscapeChar { _ }
|
||||||
|
|
||||||
// Newline item — kept out of @skip so heading detection works.
|
// Resolve ambiguities in merged states:
|
||||||
Newline { "\n" }
|
// EscapeChar > spaces: after '\', EscapeChar must win over the skip token.
|
||||||
|
// "(" > "." > "]": callSuffix delimiters must win over MarkupContent after
|
||||||
// Resolve ambiguities: more-specific tokens win over broader catch-alls.
|
// a code identifier (merged states expose these to the markup tokenizer).
|
||||||
// EscapeChar > spaces: after '\', EscapeChar must win over the skip token
|
// "_" > MarkupContent: '_' must open Emphasis rather than being swallowed
|
||||||
// (both match \t; without this, '\t' would be mis-tokenized).
|
// by MarkupContent (redundant since '_' is in MarkupContent's exclusion
|
||||||
// "(" > "." > "]" > text tokens: after '#' CodeIdent, callSuffix delimiters
|
// set, but kept for clarity).
|
||||||
// must win over MarkupContent/StrongText/EmphText in merged states.
|
// CodeIdent and StrongText/EmphText are now external tokens — not listed.
|
||||||
// LineCommentContent and MathContent are external tokens — not listed here.
|
// "[" > MarkupContent: ContentBlock callSuffix wins in merged code/markup states.
|
||||||
// "_" added after CodeIdent: KeywordExpr { CodeKeyword CallExpr? } merges
|
// CodeString > MarkupContent: '"' starts a string literal after a keyword.
|
||||||
// the post-keyword state with markup states where "_" starts Emphasis.
|
// ":" > MarkupContent: keywordBody ':' wins over markup colon in code states.
|
||||||
// CodeIdent wins so '#set _name(...)' is tokenised correctly; in pure markup
|
// URL > MarkupContent: 'https://' / 'http://' wins over plain markup text.
|
||||||
// states CodeIdent is not in the valid set so "_" still opens Emphasis.
|
@precedence { CodeBool EscapeChar CodeString URL "[" ":" "(" "." "+" "]" ClosingSquare "_" spaces MarkupContent }
|
||||||
@precedence { CodeKeyword CodeBool CodeIdent EscapeChar "(" "." "]" "_" spaces MarkupContent StrongText EmphText }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@skip { spaces }
|
@skip { spaces }
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,45 @@
|
|||||||
|
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||||
|
export const
|
||||||
|
HeadingMark = 1,
|
||||||
|
HeadingTitle = 2,
|
||||||
|
RawBlockOpen = 3,
|
||||||
|
RawBlockBody = 4,
|
||||||
|
RawBlockClose = 5,
|
||||||
|
RawInlineContent = 6,
|
||||||
|
BlockCommentBody = 7,
|
||||||
|
LineCommentContent = 8,
|
||||||
|
MathContent = 9,
|
||||||
|
CodeKeyword = 10,
|
||||||
|
CodeIdent = 11,
|
||||||
|
CodeArgKey = 12,
|
||||||
|
StrongBody = 13,
|
||||||
|
EmphBody = 14,
|
||||||
|
Document = 15,
|
||||||
|
Heading = 16,
|
||||||
|
LineComment = 17,
|
||||||
|
BlockComment = 18,
|
||||||
|
RawBlock = 19,
|
||||||
|
RawInline = 20,
|
||||||
|
CodeExpr = 21,
|
||||||
|
KeywordExpr = 22,
|
||||||
|
FuncExpr = 23,
|
||||||
|
CodeArgs = 24,
|
||||||
|
CodeString = 25,
|
||||||
|
CodeNumber = 26,
|
||||||
|
CodeBool = 27,
|
||||||
|
ContentBlock = 28,
|
||||||
|
CodeBlock = 29,
|
||||||
|
InlineMath = 30,
|
||||||
|
CodeArray = 31,
|
||||||
|
AtomExpr = 32,
|
||||||
|
Strong = 33,
|
||||||
|
Emphasis = 34,
|
||||||
|
Label = 35,
|
||||||
|
LabelName = 36,
|
||||||
|
Ref = 37,
|
||||||
|
RefName = 38,
|
||||||
|
Escape = 39,
|
||||||
|
EscapeChar = 40,
|
||||||
|
URL = 41,
|
||||||
|
MarkupContent = 42,
|
||||||
|
ClosingSquare = 43
|
||||||
@@ -73,7 +73,10 @@
|
|||||||
},
|
},
|
||||||
".tok-variableName": {
|
".tok-variableName": {
|
||||||
"color": "#9b859d"
|
"color": "#9b859d"
|
||||||
|
},
|
||||||
|
".tok-attributeName": {
|
||||||
|
"color": "#F4BF75"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dark": true
|
"dark": true
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,10 @@
|
|||||||
},
|
},
|
||||||
".tok-variableName": {
|
".tok-variableName": {
|
||||||
"color": "#FF80E1"
|
"color": "#FF80E1"
|
||||||
|
},
|
||||||
|
".tok-attributeName": {
|
||||||
|
"color": "#FFD700"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dark": true
|
"dark": true
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,10 @@
|
|||||||
},
|
},
|
||||||
".tok-attributeValue": {
|
".tok-attributeValue": {
|
||||||
"color": "rgb(0, 64, 128)"
|
"color": "rgb(0, 64, 128)"
|
||||||
|
},
|
||||||
|
".tok-attributeName": {
|
||||||
|
"color": "#994409"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dark": false
|
"dark": false
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,10 @@
|
|||||||
},
|
},
|
||||||
".tok-attributeValue": {
|
".tok-attributeValue": {
|
||||||
"color": "#234A97"
|
"color": "#234A97"
|
||||||
|
},
|
||||||
|
".tok-attributeName": {
|
||||||
|
"color": "#7B3814"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dark": false
|
"dark": false
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,10 @@
|
|||||||
},
|
},
|
||||||
".tok-list": {
|
".tok-list": {
|
||||||
"color": "rgb(185, 6, 144)"
|
"color": "rgb(185, 6, 144)"
|
||||||
|
},
|
||||||
|
".tok-attributeName": {
|
||||||
|
"color": "#994409"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dark": false
|
"dark": false
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,10 @@
|
|||||||
".tok-regexp": {
|
".tok-regexp": {
|
||||||
"color": "#009926",
|
"color": "#009926",
|
||||||
"fontWeight": "normal"
|
"fontWeight": "normal"
|
||||||
|
},
|
||||||
|
".tok-attributeName": {
|
||||||
|
"color": "#735C0F"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dark": false
|
"dark": false
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,10 @@
|
|||||||
".tok-comment": {
|
".tok-comment": {
|
||||||
"fontStyle": "italic",
|
"fontStyle": "italic",
|
||||||
"color": "#00E060"
|
"color": "#00E060"
|
||||||
|
},
|
||||||
|
".tok-attributeName": {
|
||||||
|
"color": "#F4BF75"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dark": true
|
"dark": true
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,10 @@
|
|||||||
},
|
},
|
||||||
".tok-operator": {
|
".tok-operator": {
|
||||||
"color": "#EBDAB4"
|
"color": "#EBDAB4"
|
||||||
|
},
|
||||||
|
".tok-attributeName": {
|
||||||
|
"color": "#FABD2F"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dark": true
|
"dark": true
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,10 @@
|
|||||||
".tok-comment": {
|
".tok-comment": {
|
||||||
"fontStyle": "italic",
|
"fontStyle": "italic",
|
||||||
"color": "#BC9458"
|
"color": "#BC9458"
|
||||||
|
},
|
||||||
|
".tok-attributeName": {
|
||||||
|
"color": "#DA4939"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dark": true
|
"dark": true
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,10 @@
|
|||||||
},
|
},
|
||||||
".tok-variableName": {
|
".tok-variableName": {
|
||||||
"color": "#FF80E1"
|
"color": "#FF80E1"
|
||||||
|
},
|
||||||
|
".tok-attributeName": {
|
||||||
|
"color": "#d4c96e"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dark": true
|
"dark": true
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,10 @@
|
|||||||
},
|
},
|
||||||
".tok-list": {
|
".tok-list": {
|
||||||
"color": "#8F5B26"
|
"color": "#8F5B26"
|
||||||
|
},
|
||||||
|
".tok-attributeName": {
|
||||||
|
"color": "#994409"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dark": false
|
"dark": false
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,10 @@
|
|||||||
},
|
},
|
||||||
".tok-number": {
|
".tok-number": {
|
||||||
"color": "#5A5CAD"
|
"color": "#5A5CAD"
|
||||||
|
},
|
||||||
|
".tok-attributeName": {
|
||||||
|
"color": "#7B3F00"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dark": false
|
"dark": false
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,10 @@
|
|||||||
},
|
},
|
||||||
".tok-variableName": {
|
".tok-variableName": {
|
||||||
"color": "#C1C144"
|
"color": "#C1C144"
|
||||||
|
},
|
||||||
|
".tok-attributeName": {
|
||||||
|
"color": "#ACA0DC"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dark": true
|
"dark": true
|
||||||
}
|
}
|
||||||
@@ -72,7 +72,10 @@
|
|||||||
},
|
},
|
||||||
".tok-list": {
|
".tok-list": {
|
||||||
"color": "rgb(185, 6, 144)"
|
"color": "rgb(185, 6, 144)"
|
||||||
|
},
|
||||||
|
".tok-attributeName": {
|
||||||
|
"color": "#994409"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dark": false
|
"dark": false
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,10 @@
|
|||||||
},
|
},
|
||||||
".tok-attributeValue": {
|
".tok-attributeValue": {
|
||||||
"color": "#7587A6"
|
"color": "#7587A6"
|
||||||
|
},
|
||||||
|
".tok-attributeName": {
|
||||||
|
"color": "#CF6A4C"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dark": true
|
"dark": true
|
||||||
}
|
}
|
||||||
@@ -407,15 +407,19 @@ ul.project-list-filters {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
&.project-format-badge-quarto {
|
&.project-format-badge-quarto {
|
||||||
background-color: #447099;
|
background-color: #447099; // Quarto blue (PDF output)
|
||||||
|
}
|
||||||
|
|
||||||
|
&.project-format-badge-quarto-slides {
|
||||||
|
background-color: #e4637c; // RevealJS pink-red
|
||||||
}
|
}
|
||||||
|
|
||||||
&.project-format-badge-typst {
|
&.project-format-badge-typst {
|
||||||
background-color: #ee6331;
|
background-color: #239dad; // typst.app brand blue
|
||||||
}
|
}
|
||||||
|
|
||||||
&.project-format-badge-latex {
|
&.project-format-badge-latex {
|
||||||
background-color: #72994e;
|
background-color: #098842; // Overleaf brand green
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export type ProjectApi = {
|
|||||||
accessLevel: ProjectAccessLevel
|
accessLevel: ProjectAccessLevel
|
||||||
source: Source
|
source: Source
|
||||||
compiler?: ProjectCompiler
|
compiler?: ProjectCompiler
|
||||||
|
quartoFlavor?: 'revealjs' | 'pdf'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Project = MergeAndOverride<
|
export type Project = MergeAndOverride<
|
||||||
|
|||||||
Reference in New Issue
Block a user