variables and templates
This commit is contained in:
86
src/lib/nunjucksAutocomplete.ts
Normal file
86
src/lib/nunjucksAutocomplete.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { CompletionContext, CompletionResult } from '@codemirror/autocomplete'
|
||||
|
||||
const NUNJUCKS_KEYWORDS = [
|
||||
'if', 'endif', 'elif', 'else', 'for', 'endfor', 'in', 'and', 'or', 'not',
|
||||
'true', 'false', 'none', 'macro', 'endmacro', 'set', 'endset', 'block', 'endblock',
|
||||
'extends', 'include', 'import', 'with', 'endwith', 'filter', 'endfilter', 'raw', 'endraw',
|
||||
]
|
||||
|
||||
const NUNJUCKS_FILTERS = [
|
||||
'default', 'length', 'upper', 'lower', 'title', 'trim', 'join', 'replace',
|
||||
'first', 'last', 'round', 'int', 'float', 'string', 'list', 'sort', 'groupby',
|
||||
'trim', 'escape', 'safe', 'striptags', 'capitalize', 'reverse', 'batch', 'slice',
|
||||
]
|
||||
|
||||
/** Detect if position is inside {{ or {% from the start of the line */
|
||||
function insideNunjucks(doc: { getLine: (n: number) => string }, lineNo: number, pos: number): boolean {
|
||||
const line = doc.getLine(lineNo)
|
||||
const before = line.slice(0, pos)
|
||||
const openVar = before.lastIndexOf('{{')
|
||||
const openTag = before.lastIndexOf('{%')
|
||||
const closeVar = before.lastIndexOf('}}')
|
||||
const closeTag = before.lastIndexOf('%}')
|
||||
if (openVar > -1 && (closeVar === -1 || closeVar < openVar)) return true
|
||||
if (openTag > -1 && (closeTag === -1 || closeTag < openTag)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
/** Get the word fragment before the cursor for matching */
|
||||
function wordBefore(doc: { getLine: (n: number) => string }, lineNo: number, pos: number): string {
|
||||
const line = doc.getLine(lineNo)
|
||||
let start = pos
|
||||
while (start > 0 && /[\w.-]/.test(line[start - 1])) start -= 1
|
||||
return line.slice(start, pos)
|
||||
}
|
||||
|
||||
export function nunjucksCompletionSource(
|
||||
variableIds: string[],
|
||||
configTitles?: string[],
|
||||
functionIds?: string[],
|
||||
): (context: CompletionContext) => CompletionResult | null {
|
||||
return (context: CompletionContext) => {
|
||||
const { state, pos } = context
|
||||
const line = state.doc.lineAt(pos)
|
||||
if (!insideNunjucks(state.doc, line.number - 1, pos - line.from)) return null
|
||||
|
||||
const word = wordBefore(state.doc, line.number - 1, pos - line.from)
|
||||
const from = pos - word.length
|
||||
|
||||
const options: { label: string; type?: string; info?: string }[] = []
|
||||
|
||||
for (const id of variableIds) {
|
||||
if (!word || id.toLowerCase().startsWith(word.toLowerCase())) {
|
||||
options.push({ label: id, type: 'variable', info: 'Variable' })
|
||||
}
|
||||
}
|
||||
for (const id of functionIds ?? []) {
|
||||
if (!word || id.toLowerCase().startsWith(word.toLowerCase())) {
|
||||
options.push({ label: id, type: 'function', info: 'Function (e.g. ' + id + '(var, 4))' })
|
||||
}
|
||||
}
|
||||
for (const kw of NUNJUCKS_KEYWORDS) {
|
||||
if (!word || kw.startsWith(word.toLowerCase())) {
|
||||
options.push({ label: kw, type: 'keyword', info: 'Nunjucks keyword' })
|
||||
}
|
||||
}
|
||||
for (const f of NUNJUCKS_FILTERS) {
|
||||
if (!word || f.startsWith(word.toLowerCase())) {
|
||||
options.push({ label: `${f}`, type: 'function', info: `Filter: ${f}` })
|
||||
}
|
||||
}
|
||||
if (configTitles?.length) {
|
||||
for (const t of configTitles) {
|
||||
if (!word || t.toLowerCase().startsWith(word.toLowerCase())) {
|
||||
options.push({ label: t, type: 'variable', info: 'Config' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (options.length === 0) return null
|
||||
return {
|
||||
from,
|
||||
options: options.slice(0, 50),
|
||||
validFor: /^[\w.-]*$/,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,47 @@
|
||||
import { StreamLanguage } from '@codemirror/language'
|
||||
|
||||
/** Simple PlantUML stream parser for syntax highlighting in CodeMirror */
|
||||
/** Nunjucks block comment {# ... #} */
|
||||
function tokenNunjucksComment(stream: { match: (re: RegExp) => string | null; next: () => string; eol: () => boolean }) {
|
||||
if (stream.match(/^\{#/)) {
|
||||
while (!stream.eol()) {
|
||||
if (stream.match(/#\}/)) return 'comment'
|
||||
stream.next()
|
||||
}
|
||||
return 'comment'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** Nunjucks variable {{ ... }} or tag {% ... %} - tokenize the whole block */
|
||||
function tokenNunjucksBlock(stream: { match: (re: RegExp) => string | null; next: () => string; eol: () => boolean }) {
|
||||
if (stream.match(/^\{\{/)) {
|
||||
while (!stream.eol()) {
|
||||
if (stream.match(/\}\}/)) return 'variableName.special'
|
||||
stream.next()
|
||||
}
|
||||
return 'variableName.special'
|
||||
}
|
||||
if (stream.match(/^\{\%/)) {
|
||||
while (!stream.eol()) {
|
||||
if (stream.match(/%\}/)) return 'keyword'
|
||||
stream.next()
|
||||
}
|
||||
return 'keyword'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** Simple PlantUML + Nunjucks stream parser for syntax highlighting in CodeMirror */
|
||||
const plantumlParser = StreamLanguage.define({
|
||||
name: 'plantuml',
|
||||
token(stream) {
|
||||
// Nunjucks {# ... #} comment
|
||||
const nunjucksComment = tokenNunjucksComment(stream)
|
||||
if (nunjucksComment) return nunjucksComment
|
||||
// Nunjucks {{ }} and {% %}
|
||||
const nunjucksBlock = tokenNunjucksBlock(stream)
|
||||
if (nunjucksBlock) return nunjucksBlock
|
||||
|
||||
// Single-quote line comment (PlantUML)
|
||||
if (stream.match(/^'/)) {
|
||||
stream.skipToEnd()
|
||||
|
||||
Reference in New Issue
Block a user