Формы, формы, формы! Независимо от того, где я нахожусь в своей карьере — будь то моя нынешняя работа или предыдущая — формы всегда следуют за мной. Точнее, шаблонные формы.

Постановка задачи

Представьте себе, что вам нужно добавить стандартный заголовок, содержащий определенные сведения, такие как имя формы, заголовок и номер, к сотням файлов .docx. Делать это вручную было бы невероятно утомительной задачей. К счастью, я нашел программный способ сделать это.

Знакомство с библиотекой DOCX

Поиски решения привели меня к библиотеке DOCX на JavaScript. Эта библиотека позволяет вам поместить тег в файл .docx, а затем автоматически генерировать данные для этого тега. Звучит многообещающе!

Понимание структуры файла DOCX

Файл .docx по сути представляет собой ZIP-архив, содержащий файлы XML и другие ресурсы. Текст и элементы, такие как абзацы и таблицы, хранятся в формате XML в этом ZIP-архиве.

Роль PizZip

Чтобы манипулировать этими файлами, я нашел PizZip, библиотеку, которая умеет работать с ZIP-архивами. Это позволяет нам извлечь содержимое файла .docx, отредактировать XML, а затем упаковать его обратно в файл .docx.

Начало работы: настройка вашего проекта

  1. Инициализируйте новый проект Node.js:
npm init -y

2. Установите необходимые пакеты:

npm install node pizzip xml2js docx

3. Включите модули ES6, добавив "type": "module" в ваш package.json.

{
  "name": "your-package-name",
  "version": "1.0.0",
  // ...
  "type": "module",
  // ...
}

Добавление тега в файл DOCX

Сначала давайте добавим тег в наш файл шаблона DOCX, который я назвал MyDocx.docx.

Создайте новый файл tagDocx.js и добавьте следующий код:

import PizZip from 'pizzip';
import xml2js from 'xml2js';
import fs from 'fs';

// Reading the contents of the docx file as a binary content. Add the relative path of your docx file here
const content = fs.readFileSync('MyDocx.docx', 'binary');
// Initializing a new PizZip instance with the content of the .docx file
const zip = new PizZip(content);
// Extracting the main content of the document (XML format) from the .docx ZIP archive.
const docXml = zip.files['word/document.xml'].asText();
// Use xml2js to parse the extract XML content to convert it to a Javascript object
xml2js.parseString(docXml, (err, result) => {
  // If there's an error during XML parsing, throw it.
  if (err) throw err;
  // Creating a new text element with a specific structure for .docx files.
  const newText = {
    'w:r': {
      'w:t': '{{ my_tag_here }}',
    },
  };
  // Adding the new text element to the beginning of the document body.
  result['w:document']['w:body'][0]['w:p'].unshift(newText);
  // Initializing a new XML builder to convert the modified JavaScript object back to XML format.
  const builder = new xml2js.Builder();
  // Building the updated XML content from the modified JavaScript object.
  const updatedDocXml = builder.buildObject(result);
  // Replacing the original document content in the .docx ZIP archive with the updated XML content.
  zip.file('word/document.xml', updatedDocXml);
  // Generating the updated .docx content from the modified ZIP archive.
  const updatedDocxContent = zip.generate({ type: 'nodebuffer' });
  // Writing the updated .docx content back to the original file location.
  fs.writeFileSync('MyDocx.docx', updatedDocxContent);
});

запустите этот файл:

node tagDocx.js

Теперь, если вы откроете MyDocx.docx, вы должны увидеть свой тег вверху!

Замена тега динамическими данными

Теперь, когда у нас есть тег, мы можем использовать функцию patchDocument библиотеки DOCX, чтобы заменить его динамическими данными. В моем случае мне нужно было заменить тег таблицей.

создайте еще один файл:

touch createTable.js

Вставьте в файл следующий код:

import * as fs from 'fs';
import {
  Paragraph,
  patchDocument,
  PatchType,
  Table,
  TableCell,
  TableRow,
  TextDirection,
  WidthType,
} from 'docx';

const editDocx =  async ({
  date,
  title,
  formNumber,
}) => {
  try {
    const doc = await patchDocument(fs.readFileSync('MyDocx.docx'), {
      patches: {
        my_tag_here: {
          type: PatchType.DOCUMENT,
          children: [
            new Table({
              columnWidths: [3505, 5505],
              rows: [
                new TableRow({
                  children: [
                    new TableCell({
                      children: [new Paragraph({ text: 'Date' }), new Paragraph({})],
                      textDirection: TextDirection.LEFT_TO_RIGHT_TOP_TO_BOTTOM,
                      width: {
                        size: 50,
                        type: WidthType.PERCENTAGE,
                      },
                    }),
                    new TableCell({
                      children: [new Paragraph({ text: date }), new Paragraph({})],
                      textDirection: TextDirection.LEFT_TO_RIGHT_TOP_TO_BOTTOM,
                      width: {
                        size: 50,
                        type: WidthType.PERCENTAGE,
                      },
                    }),
                  ],
                }),
                new TableRow({
                  children: [
                    new TableCell({
                      children: [new Paragraph({ text: 'Title' }), new Paragraph({})],
                      textDirection: TextDirection.LEFT_TO_RIGHT_TOP_TO_BOTTOM,
                      width: {
                        size: 50,
                        type: WidthType.PERCENTAGE,
                      },
                    }),
                    new TableCell({
                      children: [new Paragraph({ text: title}), new Paragraph({})],
                      textDirection: TextDirection.LEFT_TO_RIGHT_TOP_TO_BOTTOM,
                      width: {
                        size: 50,
                        type: WidthType.PERCENTAGE,
                      },
                    }),
                  ],
                }),
                new TableRow({
                  children: [
                    new TableCell({
                      children: [new Paragraph({ text: 'Form Number' }), new Paragraph({})],
                      textDirection: TextDirection.LEFT_TO_RIGHT_TOP_TO_BOTTOM,
                      width: {
                        size: 50,
                        type: WidthType.PERCENTAGE,
                      },
                    }),
                    new TableCell({
                      children: [new Paragraph({ text: formNumber}), new Paragraph({})],
                      textDirection: TextDirection.LEFT_TO_RIGHT_TOP_TO_BOTTOM,
                      width: {
                        size: 50,
                        type: WidthType.PERCENTAGE,
                      },
                    }),
                  ],
                }),
              ],
            }),
          ],
        },
      },
    });
    fs.writeFileSync('MyDocx.docx', doc);
  } catch (error) {
    console.error(`Error: ${error}`);
  }
};

editDocx({ date: '8/23/23', title: 'Form Title', formNumber: 'Form Number' })
  .then(() => {
    console.log('Document edited successfully.');
  })
  .catch((error) => {
    console.error(`Failed to edit document: ${error}`);
  });

Более подробно о том, как работает функция patchDocument, вы можете узнать в этой документации.

Обратите внимание, что я поместил основную функциональность в функцию более высокого порядка. Это позволяет мне динамически передавать такие параметры, как имя, заголовок и номер.

Запустите этот скрипт:

node createTable.js

Теперь просмотрите MyDocx.docx, и вы увидите таблицу с двумя столбцами и тремя строками!

От ручного присвоения тегов документам на моей предыдущей работе до автоматического выполнения этого с помощью кода — для меня это был момент полного цикла. Автоматизация задач, которые когда-то были обыденными и трудоемкими, кажется победой. Счастливые слезы! 🥲