Skip to content

Commit 36e9643

Browse files
authored
Merge pull request #71 from arduino/sebromero/linter-quickfix-assets
Add function to "quick fix" feature of the linter to remove unused images
2 parents 7d84d77 + e1fbd4f commit 36e9643

File tree

8 files changed

+122
-27
lines changed

8 files changed

+122
-27
lines changed

.vscode/launch.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"type": "pwa-node",
9+
"request": "launch",
10+
"name": "Debug Linter",
11+
"skipFiles": [
12+
"<node_internals>/**"
13+
],
14+
"program": "validate.js",
15+
"args": ["-p", "../../content"],
16+
"cwd": "${workspaceFolder}/scripts/validation/"
17+
}
18+
]
19+
}

content/hardware/04.pro/carriers/portenta-breakout/tutorials/getting-started/content.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
---
22
title: Getting Started With the Arduino Portenta Breakout
3-
coverImage: assets/ec_ard_gs_cover.svg
43
difficulty: easy
54
tags: [Getting Started, Setup, PWM, Analog, I2C]
65
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.

content/hardware/05.nicla/boards/nicla-sense-me/tutorials/cli-tool/content.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
---
22
title: Sensors Readings on a Local Webserver
3-
coverImage: assets/por_ard_usbh_cover.svg
43
difficulty: intermediate
54
tags: [Bluetooth®, WEBAPP, CLI, Installation]
65
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.

scripts/validation/domain/article.js

Lines changed: 73 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export class Article {
1717
this._markdown = null;
1818
this._metaData = null;
1919
this._codeBlockData = null;
20+
this._referencedAssetsPaths = null;
2021
}
2122

2223
get path(){
@@ -175,36 +176,90 @@ export class Article {
175176
return data.length == 0 ? null : data;
176177
}
177178

179+
/**
180+
* Returns a list of all asset file paths that are not referenced in an article
181+
*/
182+
get unreferencedAssetsPaths(){
183+
const referencedAssetNames = this.referencedAssetsPaths.map(assetPath => path.basename(assetPath));
184+
return this.assets.filter((filePath) => { return !referencedAssetNames.includes(path.basename(filePath)); });
185+
}
186+
187+
/**
188+
* Returns an array of all images and video files referenced
189+
* in the article including its meta data.
190+
*/
178191
get referencedAssetsPaths(){
179-
const images = this.html.querySelectorAll("img");
180-
const imagePaths = images.map(image => image.attributes.src);
192+
if(this._referencedAssetsPaths) return this._referencedAssetsPaths;
193+
const imagePaths = this.referencedImages;
194+
195+
const pathRegex = new RegExp(`^(?!http).*(${this.assetsFolder})\/.*(?:\..{1,4})$`);
196+
const filteredFilePaths = this.links.filter((link) => link.match(pathRegex));
197+
181198
const videos = this.html.querySelectorAll("video source");
182199
const videoPaths = videos.map(video => video.attributes.src);
183-
return imagePaths.concat(videoPaths);
200+
201+
const allPaths = imagePaths.concat(videoPaths).concat(filteredFilePaths);
202+
let coverImagePath = this.metadata?.coverImage;
203+
if(coverImagePath) allPaths.push(coverImagePath);
204+
this._referencedAssetsPaths = allPaths;
205+
return this._referencedAssetsPaths;
184206
}
185207

186-
get linkPaths(){
187-
let links = this.html.querySelectorAll("a");
188-
return links.map(link => link.attributes.href);
208+
/**
209+
* Returns all hyperlinks in the document
210+
*/
211+
get links(){
212+
let linkElements = this.html.querySelectorAll("a");
213+
return linkElements.map(element => element.attributes.href);
189214
}
190215

216+
191217
/**
192-
* Returns the assets path if it's one of the standard ones 'assets' or 'images', null otherwise.
218+
* Determines the assets folder used by an article
193219
*/
194-
get assetsPath(){
195-
if(this._assetsPath) return this._assetsPath;
220+
get assetsFolder(){
221+
if(this._assetFolder) return this._assetFolder;
196222
const validDirectories = ["assets", "images"];
197-
let path = `${this.path}/${validDirectories[0]}/`;
198223

199-
if (!existsSync(path)) {
200-
path = `${this.path}/${validDirectories[1]}/`;
201-
if(!existsSync(path)){
202-
console.log(`😬 WARNING: No standard assets directory (${validDirectories.join(" | ")}) found in: ${this.path}`);
203-
return null;
204-
}
205-
console.log("😬 WARNING: Using deprecated 'images' directory to store assets. Location:", path);
224+
if (existsSync(`${this.path}/${validDirectories[0]}/`)){
225+
this._assetFolder = validDirectories[0];
226+
return this._assetFolder;
206227
}
207-
this._assetsPath = path;
228+
if (existsSync(`${this.path}/${validDirectories[1]}/`)){
229+
console.log("😬 WARNING: Using deprecated 'images' directory to store assets. Location:", this.path);
230+
this._assetFolder = validDirectories[1];
231+
return this._assetFolder;
232+
}
233+
234+
console.log(`😬 WARNING: No standard assets directory (${validDirectories.join(" | ")}) found in: ${this.path}`);
235+
236+
// Try to figure out assets path from the referenced images
237+
const usedAssetPaths = this.referencedImages.map((assetPath) => {
238+
const directory = path.dirname(assetPath)
239+
if(!directory) return null;
240+
return directory.split("/")[0];
241+
})
242+
243+
const uniqueAssetPaths = usedAssetPaths.filter((element, index) => { return usedAssetPaths.indexOf(element) == index; });
244+
if(uniqueAssetPaths.length == 1) return uniqueAssetPaths[0];
245+
return null;
246+
}
247+
248+
/**
249+
* Returns a list of referenced images in the article
250+
*/
251+
get referencedImages(){
252+
const images = this.html.querySelectorAll("img");
253+
return images.map(image => image.attributes.src);
254+
}
255+
256+
/**
257+
* Returns the assets path if it's one of the standard ones 'assets' or 'images', null otherwise.
258+
*/
259+
get assetsPath(){
260+
if(this._assetsPath) return this._assetsPath;
261+
if(!this.assetsFolder) return null;
262+
this._assetsPath = `${this.path}/${this.assetsFolder}/`;
208263
return this._assetsPath;
209264
}
210265

scripts/validation/fix-issues.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { fixMissingTitleCase } from './fixes/headings.js'
22
import { ConfigManager } from './logic/config-manager.js';
33
import { ArticleManager } from './logic/article-manager.js';
4-
import commandLineArgs from 'command-line-args';
4+
import { fixUnusedAssets } from './fixes/assets.js';
55

66
const configManager = new ConfigManager();
77
configManager.addConfigFile("generic", "./config/config-generic.yml");
@@ -22,4 +22,8 @@ for(let article of allArticles){
2222
if(fixMissingTitleCase(article)){
2323
console.log(`✅ Fixed missing Title Case headings in '${article.contentFilePath}'.`);
2424
}
25+
26+
if(fixUnusedAssets(article)){
27+
console.log(`✅ Fixed unused assets in '${article.contentFilePath}'.`);
28+
}
2529
}

scripts/validation/fixes/assets.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import fs from 'fs';
2+
3+
function fixUnusedAssets(article){
4+
const assets = article.unreferencedAssetsPaths;
5+
if(assets.length == 0) return false;
6+
7+
for(let filePath of assets){
8+
try {
9+
console.log(`🔧 Deleting unused asset ${filePath}`);
10+
fs.unlinkSync(filePath)
11+
} catch (error) {
12+
console.error(`❌ Couldn't delete unused asset ${filePath}`);
13+
return false;
14+
}
15+
}
16+
return true;
17+
}
18+
19+
export { fixUnusedAssets };

scripts/validation/validate.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ArticleManager } from './logic/article-manager.js';
44
import { validateDuplicatedOpeningHeading, validateHeadingsNesting, validateMaxLength, validateNumberedHeadings, validateOpeningHeadingLevel, validateSpacing, validateTitleCase } from './validations/headings.js'
55
import { validateMetaData } from './validations/metadata.js';
66
import { validateRules } from './validations/rules.js';
7-
import { validateImageDescriptions, validateImagePaths, validateReferencedImages, validateSVGFiles } from './validations/images.js';
7+
import { validateImageDescriptions, validateImagePaths, validateReferencedAssets, validateSVGFiles } from './validations/assets.js';
88
import { validateSyntaxSpecifiers } from './validations/code-blocks.js';
99
import { validateNestedLists } from './validations/lists.js';
1010
import { validateBrokenLinks } from './validations/links.js';
@@ -57,7 +57,7 @@ if(configManager.options.checkBrokenLinks){
5757
};
5858

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

6262
// Verify that the images exist and don't have an absolute path
6363
validator.addValidation(allArticles, validateImagePaths);

scripts/validation/validations/images.js renamed to scripts/validation/validations/assets.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,17 @@ function validateImagePaths(article){
4646
return errorsOccurred;
4747
}
4848

49-
function validateReferencedImages(article){
49+
function validateReferencedAssets(article){
5050
let errorsOccurred = [];
5151
let imageNames = article.referencedAssetsPaths.map(imagePath => basename(imagePath));
5252
let assetNames = article.assets.map(asset => basename(asset));
53-
let linkNames = article.linkPaths.map(link => basename(link));
53+
let linkNames = article.links.map(link => basename(link));
5454
let coverImagePath = article.metadata?.coverImage;
5555
let coverImageName = coverImagePath ? basename(coverImagePath) : null;
5656

5757
assetNames.forEach(asset => {
5858
if(coverImageName == asset) return;
59-
if(!imageNames.includes(asset) && !linkNames.includes(asset)){
59+
if(!imageNames.includes(asset) && !linkNames.includes(asset)){
6060
const errorMessage = `Asset '${asset}' is not used.`;
6161
errorsOccurred.push(new ValidationIssue(errorMessage, article.contentFilePath));
6262
}
@@ -83,4 +83,4 @@ function validateSVGFiles(article){
8383
return errorsOccurred;
8484
}
8585

86-
export { validateImageDescriptions, validateImagePaths, validateReferencedImages, validateSVGFiles }
86+
export { validateImageDescriptions, validateImagePaths, validateReferencedAssets, validateSVGFiles }

0 commit comments

Comments
 (0)