const fs = require('fs');
const path = require('path');
// This script is inspired by the old perl script here:
// https://github.com/processing/processing-docs/blob/master/generate/keywords_create.cgi
// TODO: Go through base and check if there are some things we can remove
// and just rely on generated code instead:
// - String
// - All Data > Primitive
const folder = path.join(
__dirname,
'..',
'content',
'references',
'translations',
'en',
'processing'
);
/**
A list of names to ignore, based on the old perl script.
An entry such a "height" will ignore both `height` and `PImage.height`
**/
const ignore = ['width', 'height', 'x', 'y', 'z', 'Object'];
/**
A list of global functions that need to have special handling
**/
const globalFuncs = [
'keyPressed()',
'keyReleased()',
'keyTyped()',
'mouseClicked()',
'mouseDragged()',
'mouseMoved()',
'mousePressed()',
'mouseReleased()',
'mouseWheel()',
'settings()',
'setup()',
'draw()'
];
/**
This script creates the keywords.txt file that is used to do syntax highligting
in the Processing IDE.
**/
const updateKeywords = () => {
if (!processingRepoExists()) {
console.error(
'To run this script, you must have the processing4 repo next to the processing-website repo on your computer.'
);
return;
}
// Load the base keywords
const [baseKeywords, baseContents] = parseKeywordsFile('keywords_base.txt');
// Load all json files
const entries = fs.readdirSync(folder).map((file) => {
let entry = require(path.join(folder, file));
entry = Object.assign(
{
filename: file,
basename: path.basename(file, '.json')
},
entry
);
entry.leftName = leftName(entry);
entry.rightName = rightName(entry);
entry.token = getToken(entry);
return entry;
});
// Sort by listing in original keywords.txt
entries.sort(sortByClassAndGlobal);
// Run through and add if not already in base
const keywords = [];
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
if (shouldInclude(baseKeywords, entry)) {
keywords.push([entry.leftName, entry.token, entry.rightName]);
}
}
const pad = (str, size) => (str ? str.padEnd(size, ' ') : str);
// Save to keywords.txt file
const combined = baseContents + '\n\n' + generateKeywordsFile(keywords);
fs.writeFileSync(
path.join(__dirname, '..', '..', 'processing4', 'java', 'keywords.txt'),
combined
);
console.log('keywords.txt file written to the processing4 repo!');
};
/**
Sorts the entries based on the basename
**/
const sortByClassAndGlobal = (a, b) => {
const strippedA = a.basename.replace(/_$/, '').toLowerCase();
const strippedB = b.basename.replace(/_$/, '').toLowerCase();
if (strippedA < strippedB) {
return -1;
}
if (strippedA > strippedB) {
return 1;
}
return 0;
};
/**
Looks to see if the entry is in the base keywords.
**/
const shouldInclude = (keywords, entry) => {
// Don't include ignored names
if (ignore.indexOf(entry.name) > -1) {
return false;
}
// See if entry is in the keywords base
for (let i = 0; i < keywords.length; i++) {
if (keywords[i][0] == entry.leftName && keywords[i][2] == entry.basename) {
return false;
}
}
// Otherwise, include
return true;
};
/**
Parse a keywords file into an array of arrays
**/
const parseKeywordsFile = (filename) => {
const content = fs.readFileSync(path.join(__dirname, filename)).toString();
const lines = content.split('\n');
const keywords = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line[0] !== '#' && line.length > 0) {
keywords.push(line.split(/\s/));
}
}
return [keywords, content];
};
const generateKeywordsFile = (keywords) => {
let str = '';
for (let i = 0; i < keywords.length; i++) {
str += `${keywords[i][0]}\t${keywords[i][1]}\t${keywords[i][2]}\n`;
}
return str;
};
/**
Handle special cases
**/
const specialCases = {
'?: (conditional)': '?',
'/** */ (doc comment)': '/**',
'/* */ (multiline comment)': '/*'
};
/**
Makes the clean version of the name for the left-hand side of the file
**/
const operatorRegex = /\s\([a-zA-Z\s]+\)$/;
const leftName = (entry) => {
// Special handling
if (specialCases[entry.name]) {
return specialCases[entry.name];
}
// Operators are renamed: += (add assign) => +=
if (entry.name.match(operatorRegex)) {
return entry.name.replace(operatorRegex, '');
}
// Functions and class methods are renamed: float() => float
if (entry.type === 'function' || entry.type === 'method') {
return entry.name.replace(/\(\)$/, '');
}
// Variables are renamed: pixels[] => pixels
if (entry.type === 'field' || entry.type === 'other') {
return entry.name.replace(/\[\]$/, '');
}
// Just use name (Constants, classes, primitives, etc)
return entry.name;
};
/**
Make a name suitable for the right hand side
**/
const rightName = (entry) => {
// global functions appear as just the name in the right
if (globalFuncs.includes(entry.name)) {
return entry.name.replace(/\(\)$/, '');
}
// Everything else uses the basename (fill_, PVector_add_, etc)
return entry.basename;
};
/**
Gets the syntax category. Based on the old pearl script.
**/
const getToken = (entry) => {
if (globalFuncs.includes(entry.name)) {
return 'FUNCTION4';
} else if (entry.type === 'class') {
return 'KEYWORD5';
} else if (entry.type === 'method') {
return 'FUNCTION2';
} else if (entry.type === 'other') {
return 'KEYWORD4';
} else if (entry.type === 'field') {
return 'KEYWORD2';
}
return 'FUNCTION1';
};
/**
Checks whether the processing4 repo is next to this repo
**/
const processingRepoExists = (keywords) => {
const siblings = fs.readdirSync(path.join(__dirname, '..', '..'));
return siblings.includes('processing4');
};
updateKeywords();