xops-pat
Version:
Procedure generator for on space operations
449 lines (384 loc) • 11.8 kB
JavaScript
'use strict';
const fs = require('fs');
const docx = require('docx');
const childProcess = require('child_process');
const Series = require('../model/series');
module.exports = class Writer {
constructor(program, procedure) {
let task;
this.program = program;
this.procedure = procedure;
// NOTE: 720 = 1/2 inch
// 360 = 1/4
// 180 = 1/8
// 90 = 1/16
this.initialIndent = 45;
this.indentStep = 360;
// const tabOffset = 360;
// how far left of the up-pointing arrow the down-pointing arrow should be
this.hanging = 360;
this.levelTypes = [
'decimal',
'lowerLetter',
'decimal',
'lowerLetter',
'decimal',
'lowerLetter',
'decimal',
'lowerLetter',
'decimal',
'lowerLetter'
];
this.levels = [];
this.doc = this.getDoc();
this.taskNumbering = null; // gets set by getNumbering...better to return value
this.getNumbering();
for (task of this.procedure.tasks) {
this.renderTask(task);
}
}
/**
* Detect and return what columns are present on a task. A given task may
* have 1 or more columns. Only return those present in a task.
*
* @param {Task} task Task object holding columns/steps
* @param {Array} docColumns The full list of possible columns, in the proper
* order
* @return {Array} Array of column names in this procedure
*/
getTaskColumns(task, docColumns) {
const stepRows = task.concurrentSteps;
const taskColumns = [];
const taskColumnsHash = {};
let stepRow,
colName;
// Loop over the array of stepRows, and within that loop over each object of
// colName:[array,of,steps].
//
// stepRows = [
// { IV: [Step, Step, Step] }, // stepRow 0
// { IV: [Step], EV1: [Step, Step] }, // stepRow 1
// { EV1: [Step, Step], EV2: [Step] } // stepRow 2
// ]
//
for (stepRow of stepRows) {
for (colName in stepRow) {
if (!taskColumnsHash[colName]) {
// insert into a hash table because lookup is faster than array
taskColumnsHash[colName] = true;
}
}
}
// create taskColumns in order specified by procedure
for (colName of docColumns) {
if (taskColumnsHash[colName]) {
taskColumns.push(colName);
}
}
return taskColumns;
}
insertStep(series /* FIXME was cell */, step, level = 0) {
// writeStep:
// step.text via markdownformatter
// loop over step.checkboxes via markdownformatter
// FIXME: loop over images
if (step.title) {
series.container.addParagraph({
text: step.title.toUpperCase()
});
}
if (step.warnings.length) {
series.container.addBlock('warning', step.warnings, this.taskNumbering);
}
if (step.cautions.length) {
series.container.addBlock('caution', step.cautions, this.taskNumbering);
}
if (step.notes.length) {
series.container.addBlock('note', step.notes, this.taskNumbering);
}
if (step.comments.length) {
series.container.addBlock('comment', step.comments, this.taskNumbering);
}
if (step.text) {
series.container.addParagraph({
// FIXME: seems odd place for markupFilter, but "necessary"
// because containerWriter needs access.
text: series.container.markupFilter(step.text),
numbering: {
num: this.taskNumbering.concrete,
level: level
}
});
}
if (step.substeps.length) {
for (const substep of step.substeps) {
this.insertStep(series, substep, level + 1);
}
}
if (step.checkboxes.length) {
for (const checkstep of step.checkboxes) {
series.container.addParagraph({
text: series.container.markupFilter(`☐ ${checkstep}`),
numbering: {
num: this.taskNumbering.concrete,
level: level + 1
}
});
}
}
}
/*
If you want step numbers like 3.5.2
function getLongStepString (levelIndex) {
let output = '';
for (let i = 0; i <= levelIndex; i++) {
let levelValue = i + 1;
output += `%${levelValue}.`;
}
return output;
}
*/
/**
* FIXME: Instead of using child_process, dig into .git directory. Or use
* an npm package for dealing with git\
*
* FIXME: This does not currently account for changes to working directory.
*
* ADD FEATURE: Consider `git describe --tags` if tags are available. That
* will be easier for people to understand if a version they are looking at
* is significantly different. Something like semver. If version is = X.Y.Z,
* then maybe version changes could be:
*
* Changes to X = Adding/removing significant tasks from a procedure
* Changes to Y = ??? Adding/removing/modifying steps or adding/removing
* insignificant tasks.
* Changes to Z = Fixes and minor clarifications. Changes should not
* affect what crew actually do.
*
* @return {string} First 8 characters of git hash for project
*/
getGitHash() {
if (this.gitHash) {
return this.gitHash;
}
if (fs.existsSync(this.program.gitPath)) {
try {
this.gitHash = childProcess
.execSync(`cd ${this.program.projectPath} && git rev-parse HEAD`)
.toString().trim().slice(0, 8);
} catch (err) {
console.error(err);
}
return this.gitHash;
} else {
return 'NO VERSION (NOT CONFIG MANAGED)';
}
}
getGitDate() {
if (this.gitDate) {
return this.gitDate;
}
if (fs.existsSync(this.program.gitPath)) {
try {
this.gitDate = childProcess
.execSync(`cd ${this.program.projectPath} && git log -1 --format=%cd --date=iso8601`)
.toString().trim();
} catch (err) {
console.error(err);
}
return this.gitDate;
} else {
return 'NO DATE (NOT CONFIG MANAGED)';
}
}
writeFile(filepath) {
docx.Packer.toBuffer(this.doc).then((buffer) => {
fs.writeFileSync(filepath, buffer);
});
}
getNumbering() {
this.taskNumbering = {};
// const numbering = new docx.Numbering();
// const abstractNum = numbering.createAbstractNumbering();
// const abstractNum = doc.Numbering.createAbstractNumbering();
this.taskNumbering.abstract = this.doc.Numbering.createAbstractNumbering();
for (let i = 0; i < 3; i++) {
// var stepText = getLongStepString(i);
var indents = this.getIndents(i);
this.levels[i] = this.taskNumbering.abstract.createLevel(
i, this.levelTypes[i], `%${i + 1}.`, 'left'
);
this.levels[i].indent({ left: indents.left, hanging: indents.hanging });
this.levels[i].leftTabStop(indents.tab);
}
this.taskNumbering.concrete = this.doc.Numbering.createConcreteNumbering(
this.taskNumbering.abstract
);
}
getIndents(levelIndex) {
const left = this.initialIndent + (levelIndex * this.indentStep) + this.hanging;
const tab = left;
const output = {
left: left,
tab: tab,
hanging: this.hanging
};
return output;
}
getLastModifiedBy() {
return ''; // FIXME: get this from git repo if available
}
getDoc() {
const docMeta = {
title: this.procedure.procedure_name,
lastModifiedBy: this.getLastModifiedBy(),
creator: this.program.fullName
};
if (this.procedure.description) {
docMeta.description = this.procedure.description; // FIXME: not implemented
}
const doc = new docx.Document(docMeta);
doc.Styles.createParagraphStyle('normal', 'Normal')
.basedOn('Normal')
.next('Normal')
.font('Arial')
.quickFormat()
.size(20)
.spacing({
// line: 276,
before: 0, // 20 * 72 * 0.05,
after: 0 // 20 * 72 * 0.05
});
doc.Styles.createParagraphStyle('listparagraph', 'List Paragraph')
.basedOn('List Paragraph')
.next('List Paragraph')
.font('Arial')
.quickFormat()
.size(20)
.spacing({
// line: 276,
before: 0, // 20 * 72 * 0.05,
after: 0 // 20 * 72 * 0.05
});
doc.Styles.createParagraphStyle('strong', 'Strong')
.basedOn('Normal')
.next('Normal')
.font('Arial')
.bold()
.quickFormat()
.size(20)
.spacing({
// line: 276,
before: 0, // 20 * 72 * 0.05,
after: 0 // 20 * 72 * 0.05
});
return doc;
}
getPageSize() {
throw new Error('Abstract function not implemented');
}
getPageMargins() {
throw new Error('Abstract function not implemented');
}
renderTask(task) {
// FIXME: this shouldn't be hard-code, but should come from procedure
const docColumns = ['IV', 'EV1', 'EV2'];
// taskCols = ["IV", "EV1", "EV2"]
const taskCols = this.getTaskColumns(task, docColumns);
// Array of divisions. A division is a set of one or more series of
// steps. So a division may have just one series for the "IV" actor, or
// it may have multiple series for multiple actors.
//
// Example:
// divisions = [
// { IV: [Step, Step, Step] }, // div 0: just IV series
// { IV: [Step], EV1: [Step, Step] }, // div 1: IV & EV1 series
// { EV1: [Step, Step], EV2: [Step] } // div 2: EV1 & EV2 series
// ]
const divisions = task.concurrentSteps;
let division, // was "row", is a row in a 3-column table format
d, // was "r", is index for division
colName,
c,
step;
var cell;
const table = new docx.Table({
rows: task.concurrentSteps.length + 1,
columns: taskCols.length
});
for (c = 0; c < taskCols.length; c++) {
cell = table.getCell(0, c);
cell.add(new docx.Paragraph({
text: taskCols[c],
alignment: docx.AlignmentType.CENTER,
style: 'strong'
}));
}
for (d = 0; d < divisions.length; d++) {
division = divisions[d];
for (c = 0; c < taskCols.length; c++) {
colName = taskCols[c];
const series = new Series(division, colName, this.procedure);
if (series.hasSteps()) {
series.setContainer(
table.getCell(d + 1, c)
.setVerticalAlign(docx.VerticalAlign.TOP)
);
for (step of series.getSteps()) {
this.insertStep(series, step);
}
}
}
}
this.doc.addSection({
headers: { default: this.genHeader(task) },
footers: { default: this.genFooter() },
size: this.getPageSize(),
margins: this.getPageMargins(),
children: [table]
});
}
genHeader(task) {
return new docx.Header({
children: [new docx.Paragraph({
children: [new docx.TextRun({
text: `${this.procedure.name} - ${task.title} (${task.duration})`,
bold: true,
size: 24, // half-points, so double the point height
font: {
name: 'Arial'
}
})]
})]
});
}
getRightTabPosition() {
throw new Error('Abstract function not implemented');
}
genFooter() {
// const procFooter = new docx.Paragraph({ children: [] }).maxRightTabStop();
// const leftFooterText = new docx.TextRun(
// --- "Latest change: " + gitDate + " (Version: " + gitHash + ")");
// const rightFooterText = new docx.TextRun("Page").tab();
// procFooter.addRun(leftFooterText);
// procFooter.addRun(rightFooterText);
const gitDate = this.getGitDate();
const gitHash = this.getGitHash();
const footerParagraph = new docx.Paragraph({
alignment: docx.AlignmentType.LEFT,
children: [
new docx.TextRun(`${gitDate} (version: ${gitHash})`),
new docx.TextRun('Page ').pageNumber().tab(),
new docx.TextRun(' of ').numberOfTotalPages()
],
tabStop: {
right: { position: this.getRightTabPosition() }
},
style: 'normal'
}); // / .allCaps();
const procFooter = new docx.Footer({
children: [footerParagraph]
});
return procFooter;
}
};