Skip to content

Instantly share code, notes, and snippets.

@tooolbox
Last active August 29, 2015 14:27
Show Gist options
  • Save tooolbox/bbd1771c86f6b5567250 to your computer and use it in GitHub Desktop.
Save tooolbox/bbd1771c86f6b5567250 to your computer and use it in GitHub Desktop.
NodeJS for KF7 CSS Processing
#!/usr/bin/env node
// The above "shebang" tells the bash terminal to run this script as a Node.js script.
// Import NodeJS packages
var argv = require('yargs').argv; // for processing command-line arguments
var fs = require("fs"); // filesystem
var wrench = require('wrench'); // for deep recursive copying
var css = require('css'); // for parsing CSS into an AST
var walk = require('rework-walk'); // for walking ASTs generated by the css module
var cheerio = require('cheerio'); // jquery-like access to an HTML document
// Define an "ends with" method that is useful later.
if (typeof String.prototype.endsWith !== 'function') {
String.prototype.endsWith = function(suffix) {
return this.indexOf(suffix, this.length - suffix.length) !== -1;
};
}
// Fetches all files in a folder, recursively
function filesRecursive(dir) {
var results = [];
var list = fs.readdirSync(dir);
list.forEach(function(file) {
file = dir + '/' + file;
var stat = fs.statSync(file);
if (stat && stat.isDirectory()) {
results = results.concat(walk(file));
} else {
results.push(file);
}
});
return results;
}
// Same as filesRecursive, but filters for files that end with an ext, such as ".css"
function filesRecursiveWithExt(dir, ext){
var results = walk(dir);
return results.reduce(function(filteredContents, filename){
if (filename.endsWith(ext)) {
filteredContents.push(filename);
}
return filteredContents;
}, []);
}
// Convenience function:
// reads a file, calls a function that you give it,
// and then writes back to the file whatever your function returns
function readWrite(filePath, callback){
var content = '';
var stats = fs.statSync(filePath);
if (!stats.isDirectory()) {
content = fs.readFileSync(filePath).toString();
}
if (callback) {
content = callback(content, stats);
if (!stats.isDirectory() && content) {
fs.writeFileSync(filePath, content);
}
} else {
return content;
}
}
/////////////////////
// MAIN SCRIPT STARTS
/////////////////////
// Get the path to the epub directory, as an argument
var targetDirectory = argv._[0];
if (!targetDirectory) {
console.log("Please specify the directory of the epub you want to work with.");
}
var scriptDir = path.dirname(require.main.filename); // current directory of the main script
var resolvedTargetDir = path.resolve(targetDirectory);
// Clone the directory so we don't taint the original
var newDirectory = resolvedTargetDir + '_kf7';
wrench.copyDirSyncRecursive(resolvedTargetDir, newDirectory, {
forceDelete: true // overwrites any "_kf7" directory that's already there
});
// Simplify our CSS rules
var complexSelectorMap = {};
var cssFiles = filesRecursiveWithExt(newDirectory, "css");
cssFiles.forEach(function(filename){
readWrite(filename, function(content, stats){
if (!content) return;
// Parse the CSS into an AST that can be walked
var ast = css.parse(content);
// Walk the AST
walk(ast.stylesheet, function(rule, node){
if (!rule.selectors) return;
var remove = [];
var add = [];
rule.selectors.forEach(function(sel, idx){
// If the selector contains a space, it's too complex for Kindlegen
var parts = sel.split(" ");
if (parts.length > 1) {
// Create a simplified version of the selector
var newSel = '.' + parts.join('-').replace('#', '-id-').replace('.', '-clz-').replace('+', '-adj-').replace('~', '-pre-').replace('[', '-lbr-').replace(']', '-rbr-').replace(/-{2,}/, '-');
// Add it to our list to add to this rule
add.push(newSel);
// Map the complex selector to the simplified version (for later adjustments we do in the markup)
complexSelectorMap[sel] = newSel;
// Add this selector to our list to remove
remove.push(idx);
}
});
// Remove the complex selectors, if any.
// Note that we go backwards through the list, otherwise our indexes will be messed up.
for (var i = remove.length - 1; i >= 0; i--) {
rule.selectors.splice(remove[i], 1);
};
// Add the simplified selectors, if any
rule.selectors = rule.selectors.concat(add);
});
// Stringify the modified AST and return it, so it gets written back to the file
return css.stringify(ast);
});
});
// Now convert everything in the markup to the simpler selectors, as it were
var compoundClassMap = {};
var htmlFiles = filesRecursiveWithExt(newDirectory, "html"); // will include "xhtml"
htmlFiles.forEach(function(filename){
readWrite(filename, function(content, stats){
if (!content) return;
// Load up everything into Cheerio!
var $ = cheerio.load(content, {
xmlMode: true
});
// Find everything that the complex selectors applied to, and stick the simpler class on them
Object.keys(complexSelectorMap).forEach(function(key) {
$(key).addClass(complexSelectorMap[key]); // ...Well, that was easy.
});
return $.xml();
});
});
@tooolbox
Copy link
Author

More fully fledged version: https://github.com/allscribe/allscribe

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment