department-of-veterans-affairs/vets-website

View on GitHub
src/platform/pdf/templates/medications.js

Summary

Maintainability
C
1 day
Test Coverage
/**
 * Medications PDF template.
 *
 * NB: The order in which items are added to the document is important,
 * and thus PDFKit requires performing operations synchronously.
 */
/* eslint-disable no-await-in-loop */

import {
  createImageDetailItem,
  createAccessibleDoc,
  addHorizontalRule,
  createDetailItem,
  createRichTextDetailItem,
  createHeading,
  createSubHeading,
  registerVaGovFonts,
  generateInitialHeaderContent,
  generateFinalHeaderContent,
  generateFooterContent,
} from './utils';

const config = {
  margins: {
    top: 40,
    bottom: 40,
    left: 16,
    right: 32,
  },
  headings: {
    H1: {
      font: 'Bitter-Bold',
      size: 31,
    },
    H2: {
      font: 'Bitter-Bold',
      size: 23.5,
    },
    H3: {
      font: 'Bitter-Bold',
      size: 15,
    },
    H4: {
      font: 'Bitter-Bold',
      size: 12.75,
    },
  },
  subHeading: {
    boldFont: 'SourceSansPro-Bold',
    font: 'SourceSansPro-Regular',
    size: 12,
  },
  text: {
    boldFont: 'SourceSansPro-Bold',
    font: 'SourceSansPro-Regular',
    size: 12,
  },
};

const generateIntroductionContent = async (doc, parent, data) => {
  const introduction = doc.struct('Sect', {
    title: 'Introduction',
  });
  parent.add(introduction);

  // title
  introduction.add(
    createHeading(doc, 'H1', config, data.title, { x: 16, paragraphGap: 10 }),
  );

  // preface
  if (data.preface) {
    data.preface.forEach(element => {
      introduction.add(
        createSubHeading(doc, config, element.value, {
          x: 16,
          paragraphGap: element.paragraphGap ?? 6,
          ...(element.weight === 'bold' && {
            font: config.subHeading.boldFont,
          }),
          ...(element.continued && { continued: true }),
        }),
      );
    });
  }

  doc.moveDown(0.5);
  introduction.end();
};

const generateResultsMedicationListContent = async (
  medication,
  doc,
  results,
  hasHorizontalRule,
  horizontalRuleOptions = {
    spaceFromEdge: 16,
    linesAbove: 0,
    linesBelow: 1,
  },
) => {
  // medication header
  if (medication.header) {
    results.add(
      await createHeading(doc, 'H3', config, medication.header, {
        paragraphGap: 10,
        x: 16,
      }),
    );
  }

  // medication section header
  for (const section of medication.sections) {
    if (section.header) {
      results.add(
        await createHeading(
          doc,
          section.headerSize || 'H4',
          config,
          section.header,
          {
            paragraphGap: 10,
            x: 16,
          },
        ),
      );
    }

    // medication section items
    for (const resultItem of section.items) {
      let structs;
      // image item
      if (resultItem.value?.type === 'image') {
        structs = await createImageDetailItem(
          doc,
          config,
          resultItem.noIndentation ? 16 : 32,
          resultItem,
        );
        // rich text item
      } else if (resultItem.isRich) {
        structs = await createRichTextDetailItem(
          doc,
          config,
          resultItem.noIndentation ? 16 : 32,
          resultItem,
        );
        // regular item
      } else {
        structs = await createDetailItem(
          doc,
          config,
          resultItem.noIndentation ? 16 : 32,
          resultItem,
        );
      }

      // If the next item does not fit - move to the next page
      if (!resultItem.disablePageSplit) {
        doc.fontSize(config.text.size);
        let height = !resultItem.value?.type
          ? doc.heightOfString(`${resultItem.title}: ${resultItem.value}`)
          : resultItem.value?.options?.height;
        height = resultItem.inline ? height : height + 24;
        if (doc.y + height > doc.page.height - doc.page.margins.bottom)
          await doc.addPage();
      }

      for (const struct of structs) {
        results.add(struct);
      }
    }

    doc.moveDown(0.5);
  }

  // horizontal line
  if (hasHorizontalRule) {
    // if horizontal line won't fit - move to the next page
    if (doc.y > doc.page.height - doc.page.margins.bottom) await doc.addPage();
    addHorizontalRule(doc, ...Object.values(horizontalRuleOptions));
  }
};

const generateResultsContent = async (doc, parent, data) => {
  for (const resultItem of data.results) {
    const results = doc.struct('Sect', {
      title: resultItem.header || 'Results',
    });
    parent.add(results);

    // results --> header
    if (resultItem.header) {
      results.add(
        createHeading(doc, 'H2', config, resultItem.header, {
          paragraphGap: 12,
          x: 16,
        }),
      );
    }

    // results --> preface
    if (resultItem.preface && !Array.isArray(resultItem.preface)) {
      results.add(
        createSubHeading(doc, config, resultItem.preface, {
          paragraphGap: 12,
          x: 16,
        }),
      );
    } else if (resultItem.preface && Array.isArray(resultItem.preface)) {
      for (const prefaceItem of resultItem.preface) {
        results.add(
          createSubHeading(doc, config, prefaceItem.value, {
            paragraphGap: 12,
            x: 16,
          }),
        );
      }
    }

    // results --> items
    for (const listItem of resultItem.list) {
      const hasHorizontalRule = listItem.sectionSeparators !== false;
      await generateResultsMedicationListContent(
        listItem,
        doc,
        results,
        hasHorizontalRule,
        listItem.sectionSeperatorOptions,
      );
    }

    results.end();
  }
};

const generate = async data => {
  const doc = createAccessibleDoc(data, config);

  await registerVaGovFonts(doc);
  doc.addPage();

  const wrapper = doc.struct('Document');
  doc.addStructure(wrapper);

  // Add content synchronously to ensure that reading order
  // is left intact for screen reader users.

  await generateInitialHeaderContent(doc, wrapper, data, config);

  await generateIntroductionContent(doc, wrapper, data);

  if (data.results && data.results.length) {
    await generateResultsContent(doc, wrapper, data);
  }

  await generateFinalHeaderContent(doc, data, config);
  await generateFooterContent(doc, wrapper, data, config, true);

  wrapper.end();

  doc.flushPages();
  return doc;
};

export { generate };