This commit is contained in:
Matt Moyles 2026-10-09 14:26:05 -04:00 committed by GitHub
commit e75a7653b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 153 additions and 104 deletions

View File

@ -1,6 +1,7 @@
import Markdoc from '@markdoc/markdoc'
import { streamToString } from '../../util/docs.js'
import { getFile, getFileStatsIfExists, listDirectoryContentsRecursive } from '../utils/fs.js'
import { blockingFindings, markdocConfig } from '../utils/docs_markdoc.js'
import path from 'path'
import InternalServerErrorException from '#exceptions/internal_server_error_exception'
import logger from '@adonisjs/core/services/logger'
@ -45,17 +46,15 @@ export class DocsService {
parse(content: string) {
try {
const ast = Markdoc.parse(content)
const config = this.getConfig()
const errors = Markdoc.validate(ast, config)
const errors = Markdoc.validate(ast, markdocConfig)
// Filter out attribute-undefined errors which may be caused by emojis and special characters
const criticalErrors = errors.filter((e) => e.error.id !== 'attribute-undefined')
const criticalErrors = blockingFindings(errors)
if (criticalErrors.length > 0) {
logger.error('Markdoc validation errors:', errors.map((e) => JSON.stringify(e.error)).join(', '))
throw new Error('Markdoc validation failed')
}
return Markdoc.transform(ast, config)
return Markdoc.transform(ast, markdocConfig)
} catch (error) {
logger.error('Error parsing Markdoc content:', error)
throw new InternalServerErrorException(`Error parsing content: ${(error as Error).message}`)
@ -109,100 +108,4 @@ export class DocsService {
const titleCased = cleaned.replace(/\b\w/g, (char) => char.toUpperCase())
return titleCased.charAt(0).toUpperCase() + titleCased.slice(1)
}
private getConfig() {
return {
tags: {
callout: {
render: 'Callout',
attributes: {
type: {
type: String,
default: 'info',
matches: ['info', 'warning', 'error', 'success'],
},
title: {
type: String,
},
},
},
},
nodes: {
heading: {
render: 'Heading',
attributes: {
level: { type: Number, required: true },
id: { type: String },
},
},
list: {
render: 'List',
attributes: {
ordered: { type: Boolean },
start: { type: Number },
},
},
list_item: {
render: 'ListItem',
attributes: {
marker: { type: String },
className: { type: String },
class: { type: String }
}
},
table: {
render: 'Table',
},
thead: {
render: 'TableHead',
},
tbody: {
render: 'TableBody',
},
tr: {
render: 'TableRow',
},
th: {
render: 'TableHeader',
},
td: {
render: 'TableCell',
},
paragraph: {
render: 'Paragraph',
},
image: {
render: 'Image',
attributes: {
src: { type: String, required: true },
alt: { type: String },
title: { type: String },
},
},
link: {
render: 'Link',
attributes: {
href: { type: String, required: true },
title: { type: String },
},
},
fence: {
render: 'CodeBlock',
attributes: {
content: { type: String },
language: { type: String },
},
},
code: {
render: 'InlineCode',
attributes: {
content: { type: String },
},
},
hr: {
render: 'HorizontalRule',
},
},
}
}
}

View File

@ -0,0 +1,112 @@
import type { ValidateError } from '@markdoc/markdoc'
/** Markdoc schema for the bundled docs: the custom tags and the node renderers. */
export const markdocConfig = {
tags: {
callout: {
render: 'Callout',
attributes: {
type: {
type: String,
default: 'info',
matches: ['info', 'warning', 'error', 'success'],
},
title: {
type: String,
},
},
},
},
nodes: {
heading: {
render: 'Heading',
attributes: {
level: { type: Number, required: true },
id: { type: String },
},
},
list: {
render: 'List',
attributes: {
ordered: { type: Boolean },
start: { type: Number },
},
},
list_item: {
render: 'ListItem',
attributes: {
marker: { type: String },
className: { type: String },
class: { type: String },
},
},
table: {
render: 'Table',
},
thead: {
render: 'TableHead',
},
tbody: {
render: 'TableBody',
},
tr: {
render: 'TableRow',
},
th: {
render: 'TableHeader',
},
td: {
render: 'TableCell',
},
paragraph: {
render: 'Paragraph',
},
image: {
render: 'Image',
attributes: {
src: { type: String, required: true },
alt: { type: String },
title: { type: String },
},
},
link: {
render: 'Link',
attributes: {
href: { type: String, required: true },
title: { type: String },
},
},
fence: {
render: 'CodeBlock',
attributes: {
content: { type: String },
language: { type: String },
},
},
code: {
render: 'InlineCode',
attributes: {
content: { type: String },
},
},
hr: {
render: 'HorizontalRule',
},
},
}
/**
* The validation findings that should stop a doc from rendering.
*
* Warnings are not among them. Markdoc warns about a **bold** span hard-wrapped
* across lines, and transform() renders it fine, so failing on warnings turned a
* line wrap into a 500 for the whole page (#1427). attribute-undefined is
* skipped as well, since emojis and special characters can raise it.
*/
export function blockingFindings(errors: ValidateError[]): ValidateError[] {
return errors.filter(
(e) =>
e.error.id !== 'attribute-undefined' &&
(e.error.level === 'error' || e.error.level === 'critical')
)
}

View File

@ -38,9 +38,8 @@ defensive one.
**The AI is the variable, and it is the model rather than NOMAD.** Ollama idles at
roughly 1.2GB, and a model needs about its download size resident while it answers,
so an 8B model at ~4.6GB wants about that much on top. **8GB is workable for a small
model, 16GB is comfortable, and 32GB is the recommendation if you want to run larger
ones.**
so an 8B model at ~4.6GB wants about that much on top.
**8GB is workable for a small model, 16GB is comfortable, and 32GB is the recommendation if you want to run larger ones.**
Where that memory comes from depends on your hardware. With a discrete GPU the model
loads into VRAM and never touches system RAM, so VRAM is the number that limits which

View File

@ -0,0 +1,35 @@
import * as assert from 'node:assert/strict'
import { readdir, readFile } from 'node:fs/promises'
import { test } from 'node:test'
import Markdoc from '@markdoc/markdoc'
import { blockingFindings, markdocConfig } from '../../app/utils/docs_markdoc.js'
function findings(content: string) {
return blockingFindings(Markdoc.validate(Markdoc.parse(content), markdocConfig))
}
test('a bold span hard-wrapped across lines does not block rendering (#1427)', () => {
const wrapped = '**8GB is workable for a small\nmodel, 16GB is comfortable.**\n'
assert.ok(Markdoc.validate(Markdoc.parse(wrapped), markdocConfig).length > 0)
assert.deepEqual(findings(wrapped), [])
})
test('an unknown tag still blocks rendering', () => {
assert.equal(findings('{% nope %}\ntext\n{% /nope %}\n').length, 1)
})
const docsDir = new URL('../../docs/', import.meta.url)
const entries = await readdir(docsDir)
const docs = entries.filter((name) => name.endsWith('.md'))
for (const name of docs) {
test(`docs/${name} passes DocsService validation`, async () => {
const content = await readFile(new URL(name, docsDir), 'utf8')
assert.deepEqual(
findings(content).map((f) => `line ${f.lines[0] + 1}: ${f.error.message}`),
[]
)
})
}