curvenote / prosemirror-docx

Export a prosemirror document to a Microsoft Word file, using docx.
MIT License
96 stars 13 forks source link

Issue with nested list (numbered and bullets mixed) #23

Open kozborn opened 3 months ago

kozborn commented 3 months ago

There is a problem when types of list are mixed together e.g bullets list is being nested in numbered list or other way around.

image

results with

image

I was able to fix it in our code base, I'll try to prepare PR for this so you could take a look.

Btw, great job with this library, it's really helpfull

Ok, I can't push branch to do a PR so here's my code that I've changed, maybe it'll help someone

numbering.ts -- full file

import { AlignmentType, convertInchesToTwip, ILevelsOptions, LevelFormat } from 'docx';
import { INumbering } from './types';

function basicIndentStyle(indent: number): Pick<ILevelsOptions, 'style' | 'alignment'> {
  return {
    alignment: AlignmentType.START,
    style: {
      paragraph: {
        indent: { left: convertInchesToTwip(indent), hanging: convertInchesToTwip(0.18) },
      },
    },
  };
}

const numbered = (indentStart: number = 0) =>
  Array(3)
    .fill([LevelFormat.DECIMAL, LevelFormat.LOWER_LETTER, LevelFormat.LOWER_ROMAN])
    .flat()
    .map((format, level) => ({
      level,
      format,
      text: `%${level + 1}.`,
      ...basicIndentStyle(indentStart / 2),
    }));

const bullets = (indentStart: number = 0) =>
  Array(3)
    .fill(['●', '○', '■'])
    .flat()
    .map((text, level) => ({
      level,
      format: LevelFormat.BULLET,
      text,
      ...basicIndentStyle(indentStart / 2),
    }));

const styles = {
  numbered,
  bullets,
};

export type NumberingStyles = keyof typeof styles;

export function createNumbering(
  reference: string,
  style: NumberingStyles,
  indentStart: number,
): INumbering {
  return {
    reference,
    levels: styles[style](indentStart),
  };
}

and in serializer.ts

type NumberingStackItem = {
  reference: string;
  level: number;
  style: NumberingStyles;
};

...

export class DocxSerializerState {

...

currentListIndent: number = 0;
numberingStack: NumberingStackItem[] = [];

...

renderList(node: PMModel.Node, style: NumberingStyles) {
    const nextId = createShortId();

    this.numbering.push(createNumbering(nextId, style, this.currentListIndent));
    const levels: number[] = this.numberingStack
      .filter((n) => n.style === style)
      .map((n) => n.level);

    const level = Math.max(...(levels.length !== 0 ? levels : [-1])) + 1;

    this.currentListIndent += 1;
    this.numberingStack.push({ reference: nextId, level, style });

    this.renderContent(node);
    this.numberingStack.pop();
    this.currentListIndent -= 1;
  }

  // This is a pass through to the paragraphs, etc. underneath they will close the block
  renderListItem(node: PMModel.Node) {
    if (this.numberingStack.length === 0)
      throw new Error('Trying to create a list item without a list?');

    this.addParagraphOptions({
      numbering: this.numberingStack[this.numberingStack.length - 1],
    });
    this.renderContent(node);
  }

...