mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-10-17 05:14:19 +02:00
fix(docs): stop a Markdoc warning from taking a docs page down
DocsService.parse() threw on every validation finding except
attribute-undefined, warnings included. faq.md has a bold span
hard-wrapped across lines, which Markdoc reports as a child-invalid
warning ("Can't nest 'softbreak' in 'strong'"), so the FAQ page
returned a 500 even though transform() renders it fine.
Only error and critical findings block rendering now. The Markdoc
config moves to app/utils/docs_markdoc.ts so a unit test can validate
every bundled doc against the schema the service uses. The FAQ
sentence is also joined onto one line.
Fixes #1427
This commit is contained in:
parent
54ee30a449
commit
1a14695b0b
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
112
admin/app/utils/docs_markdoc.ts
Normal file
112
admin/app/utils/docs_markdoc.ts
Normal 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')
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
35
admin/tests/unit/docs_markdoc.spec.ts
Normal file
35
admin/tests/unit/docs_markdoc.spec.ts
Normal 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}`),
|
||||
[]
|
||||
)
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user