8000 Enhance template structure and sequence validation by hawkeyexl · Pull Request #2 · hawkeyexl/doc-structure-lint · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Enhance template structure and sequence validation #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e3232ae
Refactor schema to support multiple sequence item types and enhance t…
hawkeyexl Dec 3, 2024
7004601
Enhance markdown parser to include content structure for paragraphs, …
hawkeyexl Dec 3, 2024
719e78a
Add sequence validation to structure validator for enhanced template …
hawkeyexl Dec 3, 2024
aebc13e
Add sequence validation logic to ensure content order and type compli…
hawkeyexl Dec 3, 2024
61dd29a
Updated content sample
hawkeyexl Dec 3, 2024
8611763
sequence updates
hawkeyexl Dec 3, 2024
3e8ab21
Refactor sequence validation to determine content type dynamically fo…
hawkeyexl Dec 3, 2024
4a4918f
Enhance sequence validation to check length and order, improving erro…
hawkeyexl Dec 3, 2024
d4960d6
Broke sequence rule checking into smaller checks
hawkeyexl Dec 3, 2024
e053f2a
Refactor sequence validation loop for improved readability and effici…
hawkeyexl Dec 3, 2024
8c024df
Remove redundant check for unexpected content in sequence validation
hawkeyexl Dec 4, 2024
9b2b0f6
Improve error message
hawkeyexl Dec 4, 2024
7580114
Add additionalProperties constraint to sequence_item for stricter val…
hawkeyexl Dec 4, 2024
dc6258c
Update README to include validation of ordered sequences in features …
hawkeyexl Dec 4, 2024
b5bb9a7
Refactor sequence validation to use Object.hasOwn for property checks
hawkeyexl Dec 4, 2024
f7db213
Refactor addToSequence to use Object.hasOwn for property checks
hawkeyexl Dec 4, 2024
eabfa06
Update minimum paragraph requirement in templates.yaml from 3 to 2
hawkeyexl Dec 4, 2024
6befdab
Add additionalProperties constraint to sequence_item for stricter val…
hawkeyexl Dec 4, 2024
a6ba4b0
Enhance sequence validation to check for unexpected content types and…
hawkeyexl Dec 4, 2024
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Doc Structure Linter is a tool designed to validate the structure of documents (
## Features

- Validate Markdown and AsciiDoc files against custom templates
- Automatically detect file type based on extension or content
- Validate ordered sequences of content elements within sections
- Check for required sections, paragraph counts, and code block requirements
- Flexible template definitions using YAML
- Output results in both human-readable text and structured JSON formats
Expand Down Expand Up @@ -90,6 +90,7 @@ markdown-structure-linter/

Templates are defined in the `templates.yaml` file. Each template specifies the expected structure of a document, including:

- Sequencing of content within sections
- Required sections
- Paragraph count limits
- Code block requirements
Expand Down
39 changes: 35 additions & 4 deletions src/markdownParser.js
8000
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,38 @@ export function parseMarkdown(content) {
}
};

/**
* Adds a node to a section's content sequence based on the specified type.
* If the last element in the section's content is not of the specified type,
* a new sequence node is created and added to the content. Otherwise, the node
* is appended to the existing sequence of the same type.
*
* @param {Object} section - The section object containing the content array.
* @param {string} type - The type of the node to be added.
* @param {Object} node - The node to be added to the section's content.
*/
const addToSequence = (section, type, node) => {
if (
section.content.length > 0 &&
Object.hasOwn(section.content[section.content.length - 1], type)
) {
section.content[section.content.length - 1][type].push(node);
section.content[section.content.length - 1].position.end = node.position.end;
} else {
const sequenceNode = {
heading: section.heading,
position: node.position,
};
sequenceNode[type] = [node];
section.content.push(sequenceNode);
}
};

const processSection = (node) => {
const newSection = {
id: uuid(),
position: node.position,
content: [],
heading: {
level: node.depth,
position: node.position,
Expand All @@ -53,23 +81,22 @@ export function parseMarkdown(content) {

const processParagraph = (node) => {
const result = {
position: node.position,
content: node.children.map((child) => child.value).join(""),
position: node.position,
};
return result;
};

const processCodeBlock = (node) => {
const result = {
position: node.position,
content: `\`\`\`${node.lang}\n${node.value}\`\`\``,
position: node.position,
};
return result;
};

const processList = (node) => {
const result = {
position: node.position,
ordered: node.ordered,
items: node.children.map((item) => {
if (item.type === "listItem") {
Expand All @@ -88,11 +115,12 @@ export function parseMarkdown(content) {
position: child.position,
content: child.value || "",
};
}
}
}),
};
}
}),
position: node.position,
};
return result;
};
Expand Down Expand Up @@ -137,14 +165,17 @@ export function parseMarkdown(content) {
} else if (node.type === "paragraph") {
const paragraph = processParagraph(node);
updateParentPositions(parentSection, node.position.end);
addToSequence(currentSection, "paragraphs", paragraph);
currentSection.paragraphs.push(paragraph);
} else if (node.type === "code") {
const codeBlock = processCodeBlock(node);
updateParentPositions(parentSection, node.position.end);
addToSequence(currentSection, "code_blocks", codeBlock);
currentSection.codeBlocks.push(codeBlock);
} else if (node.type === "list") {
const list = processList(node);
updateParentPositions(parentSection, node.position.end);
addToSequence(currentSection, "lists", list);
currentSection.lists.push(list);
}

Expand Down
88 changes: 88 additions & 0 deletions src/rules/sequenceValidator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { validateParagraphs } from "./paragraphsValidator.js";
import { validateCodeBlocks } from "./codeBlocksValidator.js";
import { validateLists } from "./listValidator.js";

export function validateSequence(structure, template) {
const errors = [];
if (!template.sequence || !structure.content) return errors;

// Check sequence length
if (template.sequence.length !== structure.content.length) {
errors.push({
type: "sequence_length_error",
head: structure.heading.content,
message: `Expected ${template.sequence.length} content types in sequence, but found ${structure.content.length}`,
position: structure.position,
});
return errors;
}

// Check sequence order
const templateItemTypes = template.sequence.map(item => Object.keys(item)[0]);
const structureItemTypes = structure.content.map(item => {
if (Object.hasOwn(item, "paragraphs")) {
return "paragraphs";
} else if (Object.hasOwn(item, "code_blocks")) {
return "code_blocks";
} else if (Object.hasOwn(item, "lists")) {
return "lists";
} else {
return null;
}
});
// Check for unexpected content types
if (structureItemTypes.includes(null)) {
errors.push({
type: "sequence_order_error",
head: structure.heading.content,
message: `Unexpected content type (${type}) found in sequence`,
position: structure.position,
});
return errors;
}
// Check for sequence order mismatch
if (JSON.stringify(templateItemTypes) !== JSON.stringify(structureItemTypes)) {
errors.push({
type: "sequence_order_error",
head: structure.heading.content,
message: `Expected sequence ${JSON.stringify(templateItemTypes)}, but found sequence ${JSON.stringify(structureItemTypes)}`,
position: structure.position,
});
return errors;
}

// Validate each sequence item against rules
for (
let index = 0;
index < template.sequence.length;
index++
) {
const templateItem = template.sequence[index];
const structureItem = structure.content[index];
const type = templateItemTypes[index];

switch (type) {
case "paragraphs":
errors.push(...validateParagraphs(structureItem, templateItem));
break;

case "code_blocks":
errors.push(...validateCodeBlocks(structureItem, templateItem));
break;

case "lists":
errors.push(...validateLists(structureItem, templateItem));
break;

default:
errors.push({
type: "sequence_order_error",
head: structure.heading.content,
message: `Unexpected content type (${type}) found in sequence`,
position: structure.position,
});
}
}

return errors;
}
55 changes: 34 additions & 21 deletions src/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,27 +90,39 @@ export const schema = {
},
lists: {
$ref: "#/definitions/lists",
}
},
},
}
},
},
},
sequence_item: {
type: "object",
properties: {
type: {
type: "string",
enum: ["paragraph", "code_block", "list", "section"]
anyOf: [
{
additionalProperties: false,
properties: {
paragraphs: {
$ref: "#/definitions/paragraphs",
},
},
},
min: {
type: "integer",
minimum: 0
{
additionalProperties: false,
properties: {
code_blocks: {
$ref: "#/definitions/code_blocks",
},
},
},
max: {
type: "integer"
}
},
required: ["type"]
{
additionalProperties: false,
properties: {
lists: {
$ref: "#/definitions/lists",
},
},
},
],
},
section: {
description: "A section of a document demarkated by a heading",
Expand Down Expand Up @@ -148,6 +160,14 @@ export const schema = {
type: "boolean",
default: true,
},
sequence: {
description: "Ordered sequence of elements in the section",
type: "array",
minItems: 1,
items: {
$ref: "#/definitions/sequence_item",
},
},
paragraphs: {
$ref: "#/definitions/paragraphs",
},
Expand All @@ -162,13 +182,6 @@ export const schema = {
type: "boolean",
default: false,
},
sequence: {
description: "Ordered sequence of elements in the section",
type: "array",
items: {
$ref: "#/definitions/sequence_item"
}
},
sections: {
description: "Object of subsections",
type: "object",
Expand Down
6 changes: 6 additions & 0 deletions src/structureValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { validateHeading } from "./rules/headingValidator.js";
import { validateParagraphs } from "./rules/paragraphsValidator.js";
import { validateCodeBlocks } from "./rules/codeBlocksValidator.js";
import { validateLists } from "./rules/listValidator.js";
import { validateSequence } from "./rules/sequenceValidator.js";

export { validateStructure, validateSection };

Expand Down Expand Up @@ -30,6 +31,11 @@ function validateStructure(structure, template) {
function validateSection(structureSection, templateSection) {
let errors = [];

// Check sequence if defined
if (templateSection.sequence) {
errors = errors.concat(validateSequence(structureSection, templateSection));
}

// Check heading
if (templateSection.heading && structureSection.heading) {
errors = errors.concat(validateHeading(structureSection, templateSection));
Expand Down
39 changes: 21 additions & 18 deletions templates.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ info:
title: Sample Templates
version: 1.0.0

templates: # Array of templates, each with a unique key
templates: # Array of templates, each with a unique key
How-to:
sections:
Title:
Expand Down Expand Up @@ -33,19 +33,22 @@ templates: # Array of templates, each with a unique k
const: See also
paragraphs:
min: 1
Sample: # Template definition correspondings to the H1 level of the document
sections: # Array of sections, each with a unique key
Introduction: # Each sections corresponds to a heading level one below the parent, starting with an H1 at the first level
Sample: # Template definition correspondings to the H1 level of the document
sections: # Array of sections, each with a unique key
Introduction: # Each sections corresponds to a heading level one below the parent, starting with an H1 at the first level
description: A template for how-to guides that include prerequisites, setup, usage, troubleshooting, and next steps sections.
sequence:
- paragraphs:
min: 2
paragraphs:
min: 2
max: 5
code_blocks:
max: 1 # Allow a maximum of 1 code block
sections: # Array of sections, each with a unique key
Prerequisites: # Each section corresponds to a heading level one below the parent, in this case Prerequisites is an H2
heading:
const: Prerequisites # Require the section to be titled "Prerequisites"
max: 1 # Allow a maximum of 1 code block
sections: # Array of sections, each with a unique key
Prerequisites: # Each section corresponds to a heading level one below the parent, in this case Prerequisites is an H2
heading:
const: Prerequisites # Require the section to be titled "Prerequisites"
paragraphs:
max: 3
lists:
Expand All @@ -55,28 +58,28 @@ templates: # Array of templates, each with a unique k
max: 7
Setup:
paragraphs:
max: 5 # Allow a maximum of 5 paragraphs
max: 5 # Allow a maximum of 5 paragraphs
Usage:
paragraphs:
max: 4
code_blocks:
min: 1 # Require at least one code block
additionalSections: true # Allow additional use case sections
min: 1 # Require at least one code block
additionalSections: true # Allow additional use case sections
sections:
Troubleshooting:
heading:
const: Troubleshooting # Require the section to be titled "Troubleshooting"
heading:
const: Troubleshooting # Require the section to be titled "Troubleshooting"
paragraphs:
max: 5
Next steps:
$ref: '#/components/sections/Next steps' # Reference the 'Next steps' section component
$ref: "#/components/sections/Next steps" # Reference the 'Next steps' section component

components:
sections:
Next steps:
heading:
const: Next steps # Back to an H2 level section
required: false # Allow the section to be omitted
heading:
const: Next steps # Back to an H2 level section
required: false # Allow the section to be omitted
paragraphs:
min: 1
lists:
Expand Down
Loading
0