Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 19 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Debug Linter",
"skipFiles": [
"<node_internals>/**"
],
"program": "validate.js",
"args": ["-p", "../../content"],
"cwd": "${workspaceFolder}/scripts/validation/"
}
]
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
title: Getting Started With the Arduino Portenta Breakout
coverImage: assets/ec_ard_gs_cover.svg
difficulty: easy
tags: [Getting Started, Setup, PWM, Analog, I2C]
description: This tutorial will give you an overview of the core features of the breakout, setup the development environment and introduce the APIs required to program the board.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
title: Sensors Readings on a Local Webserver
coverImage: assets/por_ard_usbh_cover.svg
difficulty: intermediate
tags: [Bluetooth®, WEBAPP, CLI, Installation]
description: This tutorial teaches you how to set up the Nicla Sense ME and your computer to use the already built tool to get data and configure the board using a CLI app.
Expand Down
91 changes: 73 additions & 18 deletions scripts/validation/domain/article.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export class Article {
this._markdown = null;
this._metaData = null;
this._codeBlockData = null;
this._referencedAssetsPaths = null;
}

get path(){
Expand Down Expand Up @@ -175,36 +176,90 @@ export class Article {
return data.length == 0 ? null : data;
}

/**
* Returns a list of all asset file paths that are not referenced in an article
*/
get unreferencedAssetsPaths(){
const referencedAssetNames = this.referencedAssetsPaths.map(assetPath => path.basename(assetPath));
return this.assets.filter((filePath) => { return !referencedAssetNames.includes(path.basename(filePath)); });
}

/**
* Returns an array of all images and video files referenced
* in the article including its meta data.
*/
get referencedAssetsPaths(){
const images = this.html.querySelectorAll("img");
const imagePaths = images.map(image => image.attributes.src);
if(this._referencedAssetsPaths) return this._referencedAssetsPaths;
const imagePaths = this.referencedImages;

const pathRegex = new RegExp(`^(?!http).*(${this.assetsFolder})\/.*(?:\..{1,4})$`);
const filteredFilePaths = this.links.filter((link) => link.match(pathRegex));

const videos = this.html.querySelectorAll("video source");
const videoPaths = videos.map(video => video.attributes.src);
return imagePaths.concat(videoPaths);

const allPaths = imagePaths.concat(videoPaths).concat(filteredFilePaths);
let coverImagePath = this.metadata?.coverImage;
if(coverImagePath) allPaths.push(coverImagePath);
this._referencedAssetsPaths = allPaths;
return this._referencedAssetsPaths;
}

get linkPaths(){
let links = this.html.querySelectorAll("a");
return links.map(link => link.attributes.href);
/**
* Returns all hyperlinks in the document
*/
get links(){
let linkElements = this.html.querySelectorAll("a");
return linkElements.map(element => element.attributes.href);
}


/**
* Returns the assets path if it's one of the standard ones 'assets' or 'images', null otherwise.
* Determines the assets folder used by an article
*/
get assetsPath(){
if(this._assetsPath) return this._assetsPath;
get assetsFolder(){
if(this._assetFolder) return this._assetFolder;
const validDirectories = ["assets", "images"];
let path = `${this.path}/${validDirectories[0]}/`;

if (!existsSync(path)) {
path = `${this.path}/${validDirectories[1]}/`;
if(!existsSync(path)){
console.log(`😬 WARNING: No standard assets directory (${validDirectories.join(" | ")}) found in: ${this.path}`);
return null;
}
console.log("😬 WARNING: Using deprecated 'images' directory to store assets. Location:", path);
if (existsSync(`${this.path}/${validDirectories[0]}/`)){
this._assetFolder = validDirectories[0];
return this._assetFolder;
}
this._assetsPath = path;
if (existsSync(`${this.path}/${validDirectories[1]}/`)){
console.log("😬 WARNING: Using deprecated 'images' directory to store assets. Location:", this.path);
this._assetFolder = validDirectories[1];
return this._assetFolder;
}

console.log(`😬 WARNING: No standard assets directory (${validDirectories.join(" | ")}) found in: ${this.path}`);

// Try to figure out assets path from the referenced images
const usedAssetPaths = this.referencedImages.map((assetPath) => {
const directory = path.dirname(assetPath)
if(!directory) return null;
return directory.split("/")[0];
})

const uniqueAssetPaths = usedAssetPaths.filter((element, index) => { return usedAssetPaths.indexOf(element) == index; });
if(uniqueAssetPaths.length == 1) return uniqueAssetPaths[0];
return null;
}

/**
* Returns a list of referenced images in the article
*/
get referencedImages(){
const images = this.html.querySelectorAll("img");
return images.map(image => image.attributes.src);
}

/**
* Returns the assets path if it's one of the standard ones 'assets' or 'images', null otherwise.
*/
get assetsPath(){
if(this._assetsPath) return this._assetsPath;
if(!this.assetsFolder) return null;
this._assetsPath = `${this.path}/${this.assetsFolder}/`;
return this._assetsPath;
}

Expand Down
6 changes: 5 additions & 1 deletion scripts/validation/fix-issues.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { fixMissingTitleCase } from './fixes/headings.js'
import { ConfigManager } from './logic/config-manager.js';
import { ArticleManager } from './logic/article-manager.js';
import commandLineArgs from 'command-line-args';
import { fixUnusedAssets } from './fixes/assets.js';

const configManager = new ConfigManager();
configManager.addConfigFile("generic", "./config/config-generic.yml");
Expand All @@ -22,4 +22,8 @@ for(let article of allArticles){
if(fixMissingTitleCase(article)){
console.log(`✅ Fixed missing Title Case headings in '${article.contentFilePath}'.`);
}

if(fixUnusedAssets(article)){
console.log(`✅ Fixed unused assets in '${article.contentFilePath}'.`);
}
}
19 changes: 19 additions & 0 deletions scripts/validation/fixes/assets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import fs from 'fs';

function fixUnusedAssets(article){
const assets = article.unreferencedAssetsPaths;
if(assets.length == 0) return false;

for(let filePath of assets){
try {
console.log(`🔧 Deleting unused asset ${filePath}`);
fs.unlinkSync(filePath)
} catch (error) {
console.error(`❌ Couldn't delete unused asset ${filePath}`);
return false;
}
}
return true;
}

export { fixUnusedAssets };
4 changes: 2 additions & 2 deletions scripts/validation/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ArticleManager } from './logic/article-manager.js';
import { validateDuplicatedOpeningHeading, validateHeadingsNesting, validateMaxLength, validateNumberedHeadings, validateOpeningHeadingLevel, validateSpacing, validateTitleCase } from './validations/headings.js'
import { validateMetaData } from './validations/metadata.js';
import { validateRules } from './validations/rules.js';
import { validateImageDescriptions, validateImagePaths, validateReferencedImages, validateSVGFiles } from './validations/images.js';
import { validateImageDescriptions, validateImagePaths, validateReferencedAssets, validateSVGFiles } from './validations/assets.js';
import { validateSyntaxSpecifiers } from './validations/code-blocks.js';
import { validateNestedLists } from './validations/lists.js';
import { validateBrokenLinks } from './validations/links.js';
Expand Down Expand Up @@ -57,7 +57,7 @@ if(configManager.options.checkBrokenLinks){
};

// Verify that all files in the assets folder are referenced
validator.addValidation(allArticles, validateReferencedImages);
validator.addValidation(allArticles, validateReferencedAssets);

// Verify that the images exist and don't have an absolute path
validator.addValidation(allArticles, validateImagePaths);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,17 @@ function validateImagePaths(article){
return errorsOccurred;
}

function validateReferencedImages(article){
function validateReferencedAssets(article){
let errorsOccurred = [];
let imageNames = article.referencedAssetsPaths.map(imagePath => basename(imagePath));
let assetNames = article.assets.map(asset => basename(asset));
let linkNames = article.linkPaths.map(link => basename(link));
let linkNames = article.links.map(link => basename(link));
let coverImagePath = article.metadata?.coverImage;
let coverImageName = coverImagePath ? basename(coverImagePath) : null;

assetNames.forEach(asset => {
if(coverImageName == asset) return;
if(!imageNames.includes(asset) && !linkNames.includes(asset)){
if(!imageNames.includes(asset) && !linkNames.includes(asset)){
const errorMessage = `Asset '${asset}' is not used.`;
errorsOccurred.push(new ValidationIssue(errorMessage, article.contentFilePath));
}
Expand All @@ -83,4 +83,4 @@ function validateSVGFiles(article){
return errorsOccurred;
}

export { validateImageDescriptions, validateImagePaths, validateReferencedImages, validateSVGFiles }
export { validateImageDescriptions, validateImagePaths, validateReferencedAssets, validateSVGFiles }