Compare commits

..

16 Commits

Author SHA1 Message Date
Nick Playfair
e1dc531009 2.1.0 2025-06-12 22:24:02 +01:00
Nick Playfair
35c075e1b5 New ts-eslint config 2025-06-12 22:18:50 +01:00
Nick Playfair
9b836cc360 Remove old eslint files 2025-06-12 21:40:21 +01:00
Nick Playfair
8b65a52035 Remove some unused function plans 2025-06-12 19:08:46 +01:00
Nick Playfair
7c7ec2338b Favicon 2025-06-12 19:07:05 +01:00
Nick Playfair
c08017bd59 Prettify HTML code output 2025-06-12 16:44:44 +01:00
Nick Playfair
a3f3c2ed09 Get table HTML code 2025-06-12 16:33:03 +01:00
Nick Playfair
70e444d6a3 Filter parts using array methods instead of separate function 2025-06-12 15:35:46 +01:00
Nick Playfair
2584a58df2 2.0.0 2025-06-11 23:54:06 +01:00
Nick Playfair
fb632c1f72 remove unused import 2025-06-11 23:53:52 +01:00
Nick Playfair
744e48e812 FInish table class implementation 2025-06-11 23:42:59 +01:00
Nick Playfair
c1d38f4d0c Begin PartsTable class 2025-06-11 22:54:27 +01:00
Nick Playfair
9839232769 Implement table methods 2025-06-11 22:22:23 +01:00
Nick Playfair
59c5f71768 Change bundle link for dev server 2025-06-11 14:39:20 +01:00
Nick Playfair
21296a9b0d Use webpack to copy html and css into dist folder 2025-06-11 14:15:28 +01:00
Nick Playfair
76e5e44cc7 Fix dependencies 2025-06-11 13:49:36 +01:00
25 changed files with 2974 additions and 1479 deletions

View File

@ -1,2 +0,0 @@
node_modules/
dist/

View File

@ -1,18 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"project": "./tsconfig.json"
},
"rules": {
"no-console": "off"
}
}

View File

@ -1,7 +1,13 @@
# bom2table # bom2table
Generate a nicely formatted table from an EAGLE BOM Generate a nicely formatted table from an EAGLE BOM
## Usage ## Usage
Run `npm install` and `npm build` Run `npm install` and `npm build`
Upload a BOM exported in csv format from EAGLE (untick 'List attributes') to get a table and the corresponding HTML. Upload a BOM exported in csv format from EAGLE (untick 'List attributes') to get a table and the corresponding HTML.
## Credits
Favicon by [Round Icons](https://unsplash.com/illustrations/a-computer-chip-with-a-red-yellow-and-green-color-scheme-otzESK2sBG4?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash) via Unsplash

17
eslint.config.mjs Normal file
View File

@ -0,0 +1,17 @@
// @ts-check
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: [
'dist/**',
'node_modules/**',
'webpack.dev.js',
'webpack.prod.js',
],
},
eslint.configs.recommended,
tseslint.configs.recommended,
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
html/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
html/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

BIN
html/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
html/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -4,6 +4,10 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="style.css" /> <link rel="stylesheet" href="style.css" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
<title>Read CSV File</title> <title>Read CSV File</title>
</head> </head>
<body> <body>
@ -30,6 +34,6 @@
<pre id="partsHTML"></pre> <pre id="partsHTML"></pre>
</section> </section>
</main> </main>
<script src="dist/bundle.js"></script> <script src="bundle.js"></script>
</body> </body>
</html> </html>

1
html/site.webmanifest Normal file
View File

@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

4020
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,25 +1,29 @@
{ {
"name": "bom2table", "name": "bom2table",
"version": "1.3.0", "version": "2.1.0",
"description": "", "description": "",
"main": "bom2table.js", "main": "bom2table.js",
"scripts": { "scripts": {
"dev": "http-server",
"serve": "webpack serve --config webpack.dev.js", "serve": "webpack serve --config webpack.dev.js",
"build": "webpack --config webpack.prod.js" "build": "webpack --config webpack.prod.js",
"lint": "eslint ."
}, },
"author": "nplayfair", "author": "nplayfair",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"csvtojson": "^2.0.10", "csvtojson": "^2.0.10",
"http-server": "^14.1.1", "htmlfy": "^0.7.5"
"pretty": "^2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.28.0",
"@types/csvtojson": "^1.1.5", "@types/csvtojson": "^1.1.5",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^13.0.0",
"eslint": "^9.28.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"ts-loader": "^9.5.2", "ts-loader": "^9.5.2",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"typescript-eslint": "^8.34.0",
"webpack": "^5.99.9", "webpack": "^5.99.9",
"webpack-cli": "^6.0.1", "webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.2" "webpack-dev-server": "^5.2.2"

33
src/config.ts Normal file
View File

@ -0,0 +1,33 @@
//Array of part names to omit from the BOM
export const rejectedParts = [
'TP1',
'TP2',
'TP3',
'G',
'U$1',
'S1',
'J1',
'J2',
'JP1',
'JP2',
'V',
'I',
'O',
'T1',
'T2',
'T3',
'INPUT',
'IN',
'OUT',
];
//Header titles for the table
export const headers: string[] = ['Part', 'Value'];
//Config object for csv library
export const csvConfig = {
delimiter: `;`,
quote: '"',
ignoreEmpty: true,
includeColumns: /(Part|Value)/,
};

View File

@ -1,16 +1,9 @@
import { CSVParseParam } from '../node_modules/csvtojson/v2/Parameters'; import { csvConfig, rejectedParts } from './config';
import { isJunk } from './utils'; // const csv = require('csvtojson');
const csv = require('csvtojson'); import csv from 'csvtojson';
//CSV Config for EAGLE BOM
const csvConfig = {
delimiter: `;`,
quote: '"',
ignoreEmpty: true,
includeColumns: /(Part|Value)/,
};
export async function getBOM(csvBOM: string) { export async function getBOM(csvBOM: string) {
const bom = await csv(csvConfig).fromString(csvBOM); const rawBOM = await csv(csvConfig).fromString(csvBOM);
const bom = rawBOM.filter((part: Part) => !rejectedParts.includes(part.Part));
return bom; return bom;
} }

View File

@ -1,10 +1,17 @@
//Modules //Modules
import { getBOM } from './csvToJSON'; import { getBOM } from './csvToJSON';
import { PartsTable } from './table';
import { headers } from './config';
import { printHTMLtable } from './utils';
//DOM elements //DOM elements
const input = document.getElementById('csvInput') as HTMLInputElement; const input = document.getElementById('csvInput') as HTMLInputElement;
const csvTextOutput = document.getElementById('csvText') as HTMLPreElement; const csvTextOutput = document.getElementById('csvText') as HTMLPreElement;
let rawCSV: string; const jsonTextOutput = document.getElementById('partsJSON') as HTMLPreElement;
const partsHTML = document.getElementById('partsHTML') as HTMLPreElement;
const htmlTable = document.getElementById('partsTable') as HTMLTableElement;
let csvBOM: string;
//Functions //Functions
function handleUpload(event: Event) { function handleUpload(event: Event) {
@ -23,80 +30,34 @@ function processCSV(fileReader: Event) {
const content = (fileReader.target as FileReader).result; const content = (fileReader.target as FileReader).result;
if (content === null) throw new Error('CSV Cannot be null.'); if (content === null) throw new Error('CSV Cannot be null.');
const csvString = content.toString(); const csvString = content.toString();
csvBOM = csvString;
//Display the CSV contents //Display the CSV contents
csvTextOutput.innerText = csvString; csvTextOutput.innerText = csvString;
const bomJSON = createJSONbom(csvString); //JSON Object
printJSONbom(csvString);
//Table
createTable();
//TODO Table HTML code
} }
//TODO //Print JSON
async function createJSONbom(csvString: string) { async function printJSONbom(csvString: string) {
const bomJSON = await getBOM(csvString); const bomJSON = await getBOM(csvString);
console.log(bomJSON); jsonTextOutput.innerText = JSON.stringify(bomJSON, null, 2);
}
//Construct table
async function createTable() {
const bomJSON = await getBOM(csvBOM);
const partsTable = new PartsTable(htmlTable, headers, bomJSON);
const tableMarkup = await partsTable.createTable();
printHTMLtable(tableMarkup as HTMLTableElement, partsHTML);
} }
//Add event listener //Add event listener
input.addEventListener('change', handleUpload); input.addEventListener('change', handleUpload);
// csvToJSON();
/* TODO /* TODO
// Format the HTML nicely and output to a pre code block
function displayMarkup() {
const tableCode = document.querySelector('table')!.outerHTML;
const markup = document.getElementById('markup') as HTMLElement;
markup.innerText = beautify(tableCode);
}
// Table functions
function clearTable() {
document.querySelector('table')!.innerHTML = '';
}
function generateTableHead(table: HTMLTableElement, data: Array<string>) {
const thead = table.createTHead();
const row = thead.insertRow();
// Populate Header row
data.map((key) => {
const th = document.createElement('th');
const text = document.createTextNode(key);
th.appendChild(text);
row.appendChild(th);
});
}
function generateTableBody(table: HTMLTableElement, data: part[]) {
data.map((component) => {
const row = table.insertRow();
// Insert Part Name
const partName = row.insertCell();
const partNameText = document.createTextNode(component.Part);
partName.appendChild(partNameText);
// Insert Part Value
const partVal = row.insertCell();
const partValText = document.createTextNode(component.Value);
partVal.appendChild(partValText);
});
}
async function makeTable(csvString: string) {
const converter: Converter = csv({
delimiter: ';',
includeColumns: /(Part|Value)/,
ignoreEmpty: true,
})
const jsonArray = await converter.fromString(csvString);
// Filter out unwanted parts
const parts = jsonArray.filter(isJunk);
// Build table
const table = document.querySelector('table') as HTMLTableElement;
const headerData = Object.keys(parts[0]);
generateTableBody(table, parts);
generateTableHead(table, headerData);
displayMarkup();
}
// Create a JSON object for Contentful // Create a JSON object for Contentful
function makeJSON(csvString: string) { function makeJSON(csvString: string) {
csv({ csv({
@ -117,22 +78,4 @@ function makeJSON(csvString: string) {
); );
}); });
} }
const csvPicker = document.getElementById('csvFile') as HTMLInputElement;
csvPicker.onchange = function handleFiles(event: Event) {
const target = event.target as HTMLInputElement;
const csvFile: File = (target.files as FileList)[0];
const reader = new FileReader();
reader.readAsText(csvFile);
reader.onload = () => {
if (reader.result == null) {
throw new Error('Something went wrong.');
}
const csvString = reader.result.toString();
clearTable();
makeTable(csvString);
makeJSON(csvString);
};
}
*/ */

View File

@ -1,36 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="style.css">
<title>BOM2Table</title>
</head>
<body>
<header>
<h1>BOM to Table</h1>
</header>
<main>
<section class="upload">
<h2>Upload BOM csv file</h2>
<input id="csvFile" type="file" accept=".csv" />
<hr />
</section>
<section>
<h2>Table</h2>
<table></table>
</section>
<section>
<h2>Table HTML</h2>
<!-- <textarea id="markup" cols="30" rows="10"></textarea> -->
<pre contenteditable=""><code id="markup" class="language-html"></code></pre>
</section>
<section>
<h2>JSON Object</h2>
<pre contenteditable=""><code id="jsonObject"></code></pre>
</section>
</main>
<script src="bundle.js" defer></script>
</body>
</html>

20
src/parts.d.ts vendored
View File

@ -1,4 +1,4 @@
enum PartType { enum ComponentType {
R, R,
C, C,
D, D,
@ -7,12 +7,26 @@ enum PartType {
COMPONENT, COMPONENT,
} }
declare interface part { enum Potentiometers {
VOL = 'VOL',
TONE = 'TONE',
GAIN = 'GAIN',
RES = 'RES',
LEVEL = 'LEVEL',
DIST = 'DIST',
PRESENCE = 'PRESENCE',
}
declare interface Part {
Part: string; Part: string;
PartType: PartType;
Value: string; Value: string;
} }
declare interface Component extends Part {
Designator: string; //A letter like R, C, D etc
ComponentType: ComponentType;
}
declare interface structuredParts { declare interface structuredParts {
C: { C: {
[key: string]: string; [key: string]: string;

49
src/table.ts Normal file
View File

@ -0,0 +1,49 @@
export class PartsTable {
constructor(
public htmlTable: HTMLTableElement,
public headers: string[],
public jsonBOM: Part[],
) {}
//Reset table
public clearTable(): void {
this.htmlTable.innerHTML = '';
}
//Header
private createTableHeader(): void {
const tHead = this.htmlTable.createTHead();
const hRow = tHead.insertRow();
//Populate headers with text
for (const header of this.headers) {
const th = document.createElement('th');
const headerText = document.createTextNode(header);
th.appendChild(headerText);
hRow.appendChild(th);
}
}
//Body
private createTableBody(): void {
this.jsonBOM.map((component) => {
//Create a row
const tRow = this.htmlTable.insertRow();
//Insert part name
const partName = tRow.insertCell();
const partNameText = document.createTextNode(component.Part);
partName.appendChild(partNameText);
//Insert part value
const partValue = tRow.insertCell();
const partValText = document.createTextNode(component.Value);
partValue.appendChild(partValText);
});
}
//Create full table
public createTable(): HTMLElement {
this.clearTable();
this.createTableHeader();
this.createTableBody();
return this.htmlTable;
}
}

View File

@ -1,8 +1,16 @@
export function isJunk(element: part): boolean { import { prettify } from 'htmlfy';
// Returns true if element is in the rejected list
return rejectedParts.includes(element.Part); //Print HTML code
export async function printHTMLtable(
table: HTMLTableElement,
codeBlock: HTMLPreElement,
) {
codeBlock.innerText = prettify(table.outerHTML);
} }
// TODO
/*
export function getPartType(component: part) { export function getPartType(component: part) {
if (component.Part.match(/^C\d/) != null) { if (component.Part.match(/^C\d/) != null) {
return 'C'; return 'C';
@ -16,8 +24,7 @@ export function getPartType(component: part) {
return 'COMPONENT'; return 'COMPONENT';
} }
// TODO
/*
function getJSONParts(allParts: part[]) { function getJSONParts(allParts: part[]) {
const jsonParts: structuredParts = { const jsonParts: structuredParts = {
C: {}, C: {},
@ -46,25 +53,3 @@ function getJSONParts(allParts: part[]) {
return jsonParts; return jsonParts;
} }
*/ */
const rejectedParts = [
'TP1',
'TP2',
'TP3',
'G',
'U$1',
'S1',
'J1',
'J2',
'JP1',
'JP2',
'V',
'I',
'O',
'T1',
'T2',
'T3',
'INPUT',
'IN',
'OUT',
];

View File

@ -46,7 +46,7 @@
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
/* Module Resolution Options */ /* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */

View File

@ -1,28 +1,39 @@
const path = require("path"); const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = { module.exports = {
mode: "development", mode: 'development',
entry: "./src/index.ts", entry: './src/index.ts',
devtool: "inline-source-map", devtool: 'inline-source-map',
devServer: { devServer: {
static: { static: {
directory: path.join(__dirname, "./"), directory: path.join(__dirname, './'),
}, },
}, },
module: { module: {
rules: [ rules: [
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
use: "ts-loader", use: 'ts-loader',
exclude: /node_modules/, exclude: /node_modules/,
}, },
], ],
}, },
resolve: { resolve: {
extensions: [".tsx", ".ts", ".js"], extensions: ['.tsx', '.ts', '.js'],
}, },
output: { output: {
filename: "bundle.js", filename: 'bundle.js',
path: path.resolve(__dirname, "dist"), path: path.resolve(__dirname, 'dist'),
publicPath: "/dist", publicPath: '/dist',
}, },
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: 'html/**/*',
to: '[name][ext]',
},
],
}),
],
}; };

View File

@ -1,31 +1,42 @@
const path = require("path"); const path = require('path');
const { CleanWebpackPlugin } = require("clean-webpack-plugin"); const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = { module.exports = {
mode: "production", mode: 'production',
entry: "./src/index.ts", entry: './src/index.ts',
// devtool: "inline-source-map", // devtool: "inline-source-map",
devServer: { devServer: {
static: { static: {
directory: path.join(__dirname, "./"), directory: path.join(__dirname, './'),
}, },
}, },
module: { module: {
rules: [ rules: [
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
use: "ts-loader", use: 'ts-loader',
exclude: /node_modules/, exclude: /node_modules/,
}, },
], ],
}, },
resolve: { resolve: {
extensions: [".tsx", ".ts", ".js"], extensions: ['.tsx', '.ts', '.js'],
}, },
output: { output: {
filename: "bundle.js", filename: 'bundle.js',
path: path.resolve(__dirname, "dist"), path: path.resolve(__dirname, 'dist'),
publicPath: "/dist", publicPath: '/dist',
}, },
plugins: [new CleanWebpackPlugin()], plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [
{
from: 'html/**/*',
to: '[name][ext]',
},
],
}),
],
}; };