Skip to content

Commit

Permalink
Implement unordered and ordered lists (#88)
Browse files Browse the repository at this point in the history
* wip

* implement bullet points

* needs more ifs

* refactor

* revert

* refactor

* fix and refactor

* refactor

* refactor

* refactor

* add ordered class

* wip

* wip

* fixes

* package.json

* little refactors

* base list

* fixes

* abstract baselist

* refactors

* refactor

* changeset

* fix eslint issue

* fix

* fixes

* fixes

---------

Co-authored-by: Alex Sánchez <sion333@gmail.com>
  • Loading branch information
jordisala1991 and Cenadros committed May 7, 2024
1 parent afa47af commit 2920ac2
Show file tree
Hide file tree
Showing 44 changed files with 362 additions and 123 deletions.
5 changes: 5 additions & 0 deletions .changeset/curly-clouds-tease.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"penpot-exporter": minor
---

Ordered and unordered list support
5 changes: 4 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ module.exports = {
}
},
rules: {
'@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }]
'@typescript-eslint/no-unused-vars': [
'error',
{ ignoreRestSiblings: true, argsIgnorePattern: '^_' }
]
}
};
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"react": "^18.3",
"react-dom": "^18.3",
"react-hook-form": "^7.51",
"romans": "^2.0",
"slugify": "^1.6",
"svg-path-parser": "^1.1"
},
Expand Down
2 changes: 1 addition & 1 deletion plugin-src/code.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { transformDocumentNode } from '@plugin/transformers';

import { findAllTextNodes } from './findAllTextnodes';
import { setCustomFontId } from './translators/text/custom';
import { setCustomFontId } from './translators/text/font/custom';

figma.showUI(__html__, { themeColors: true, height: 300, width: 400 });

Expand Down
4 changes: 2 additions & 2 deletions plugin-src/findAllTextnodes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isGoogleFont } from './translators/text/gfonts';
import { isLocalFont } from './translators/text/local';
import { isGoogleFont } from './translators/text/font/gfonts';
import { isLocalFont } from './translators/text/font/local';

export const findAllTextNodes = async () => {
await figma.loadAllPagesAsync();
Expand Down
10 changes: 4 additions & 6 deletions plugin-src/transformers/partials/transformText.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { transformFills } from '@plugin/transformers/partials';
import {
transformTextStyle,
translateGrowType,
translateStyleTextSegments,
translateVerticalAlign
} from '@plugin/translators/text';
import { transformTextStyle, translateStyleTextSegments } from '@plugin/translators/text';
import { translateGrowType, translateVerticalAlign } from '@plugin/translators/text/properties';

import { TextShape } from '@ui/lib/types/shapes/textShape';

Expand All @@ -17,6 +13,8 @@ export const transformText = (node: TextNode): Partial<TextShape> => {
'letterSpacing',
'textCase',
'textDecoration',
'indentation',
'listOptions',
'fills'
]);

Expand Down
2 changes: 1 addition & 1 deletion plugin-src/translators/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from './translateBlendMode';
export * from './translateShadowEffects';
export * from './translateFills';
export * from './translateShadowEffects';
export * from './translateStrokes';
export * from './translateVectorPaths';
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getCustomFontId, translateFontVariantId } from '@plugin/translators/text/custom';
import { getCustomFontId, translateFontVariantId } from '@plugin/translators/text/font/custom';

import { FontId } from '@ui/lib/types/shapes/textShape';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './googleFont';
export * from './translateGoogleFont';
export * from './translateFontVariantId';
export * from './translateGoogleFont';
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import slugify from 'slugify';

import { translateFontVariantId } from '@plugin/translators/text/gfonts';
import { translateFontVariantId } from '@plugin/translators/text/font/gfonts';

import { FontId } from '@ui/lib/types/shapes/textShape';

Expand Down
1 change: 1 addition & 0 deletions plugin-src/translators/text/font/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './translateFontId';
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './localFont';
export * from './translateLocalFont';
export * from './translateFontVariantId';
export * from './translateLocalFont';
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LocalFont, translateFontVariantId } from '@plugin/translators/text/local';
import { LocalFont, translateFontVariantId } from '@plugin/translators/text/font/local';

import { FontId } from '@ui/lib/types/shapes/textShape';

Expand Down
10 changes: 0 additions & 10 deletions plugin-src/translators/text/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1 @@
export * from './translateFontId';
export * from './translateFontStyle';
export * from './translateGrowType';
export * from './translateHorizontalAlign';
export * from './translateLetterSpacing';
export * from './translateLineHeight';
export * from './translateParagraphProperties';
export * from './translateStyleTextSegments';
export * from './translateTextDecoration';
export * from './translateTextTransform';
export * from './translateVerticalAlign';
79 changes: 79 additions & 0 deletions plugin-src/translators/text/paragraph/List.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { StyleTextSegment } from '@plugin/translators/text/paragraph/translateParagraphProperties';

import { TextNode as PenpotTextNode } from '@ui/lib/types/shapes/textShape';

import { ListTypeFactory } from './ListTypeFactory';

type Level = {
style: PenpotTextNode;
counter: number;
type: ListType;
};

type ListType = 'ORDERED' | 'UNORDERED';

export class List {
private levels: Map<number, Level> = new Map();
private indentation = 0;
protected counter: number[] = [];
private listTypeFactory = new ListTypeFactory();

public update(textNode: PenpotTextNode, segment: StyleTextSegment): void {
if (segment.indentation < this.indentation) {
for (let i = segment.indentation + 1; i <= this.indentation; i++) {
this.levels.delete(i);
}
}

let level = this.levels.get(segment.indentation);

if (!level || level.type !== this.getListType(segment)) {
level = {
style: this.createStyle(textNode, segment.indentation),
counter: 0,
type: this.getListType(segment)
};

this.levels.set(segment.indentation, level);
}

level.counter++;
this.indentation = segment.indentation;
}

public getCurrentList(textNode: PenpotTextNode, segment: StyleTextSegment): PenpotTextNode {
const level = this.levels.get(segment.indentation);
if (level === undefined) {
throw new Error('Levels not updated');
}

const listType = this.listTypeFactory.getListType(segment.listOptions);

return this.updateCurrentSymbol(
listType.getCurrentSymbol(level.counter, segment.indentation),
level.style
);
}

private getListType(segment: StyleTextSegment): ListType {
if (segment.listOptions.type === 'NONE') {
throw new Error('List type not valid');
}

return segment.listOptions.type;
}

private createStyle(node: PenpotTextNode, indentation: number): PenpotTextNode {
return {
...node,
text: `${'\t'.repeat(Math.max(0, indentation - 1))}{currentSymbol}`
};
}

private updateCurrentSymbol(character: string, currentStyle: PenpotTextNode): PenpotTextNode {
return {
...currentStyle,
text: currentStyle.text.replace('{currentSymbol}', character)
};
}
}
3 changes: 3 additions & 0 deletions plugin-src/translators/text/paragraph/ListType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface ListType {
getCurrentSymbol(number: number, indentation: number): string;
}
19 changes: 19 additions & 0 deletions plugin-src/translators/text/paragraph/ListTypeFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ListType } from './ListType';
import { OrderedListType } from './OrderedListType';
import { UnorderedListType } from './UnorderedListType';

export class ListTypeFactory {
private unorderedList = new UnorderedListType();
private orderedList = new OrderedListType();

public getListType(textListOptions: TextListOptions): ListType {
switch (textListOptions.type) {
case 'ORDERED':
return this.orderedList;
case 'UNORDERED':
return this.unorderedList;
}

throw new Error('List type not valid');
}
}
42 changes: 42 additions & 0 deletions plugin-src/translators/text/paragraph/OrderedListType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as romans from 'romans';

import { ListType } from './ListType';

export class OrderedListType implements ListType {
public getCurrentSymbol(number: number, indentation: number): string {
let symbol = '. ';
switch (indentation % 3) {
case 0:
symbol = romans.romanize(number).toLowerCase() + symbol;
break;
case 2:
symbol = this.letterOrderedList(number) + symbol;
break;
case 1:
default:
symbol = number.toString() + symbol;
break;
}

return symbol;
}

private letterOrderedList(number: number): string {
let result = '';

while (number > 0) {
let letterCode = number % 26;

if (letterCode === 0) {
letterCode = 26;
number = Math.floor(number / 26) - 1;
} else {
number = Math.floor(number / 26);
}

result = String.fromCharCode(letterCode + 96) + result;
}

return result;
}
}
93 changes: 93 additions & 0 deletions plugin-src/translators/text/paragraph/Paragraph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { TextNode as PenpotTextNode } from '@ui/lib/types/shapes/textShape';

import { List } from './List';
import { StyleTextSegment } from './translateParagraphProperties';

export class Paragraph {
private isParagraphStarting = false;
private isPreviousNodeAList = false;
private firstTextNode: PenpotTextNode | null = null;
private list = new List();

public format(
node: TextNode,
textNode: PenpotTextNode,
segment: StyleTextSegment
): PenpotTextNode[] {
const textNodes: PenpotTextNode[] = [];

const spacing = this.applySpacing(segment, node);
if (spacing) textNodes.push(spacing);

const indentation = this.applyIndentation(textNode, segment, node);
if (indentation) textNodes.push(indentation);

textNodes.push(textNode);

this.isPreviousNodeAList = segment.listOptions.type !== 'NONE';
this.isParagraphStarting = textNode.text === '\n';

return textNodes;
}

private applyIndentation(
textNode: PenpotTextNode,
segment: StyleTextSegment,
node: TextNode
): PenpotTextNode | undefined {
if (this.isParagraphStarting || this.isFirstTextNode(textNode)) {
this.list.update(textNode, segment);

return segment.listOptions.type !== 'NONE'
? this.list.getCurrentList(textNode, segment)
: this.segmentIndent(node.paragraphIndent);
}
}

private applySpacing(segment: StyleTextSegment, node: TextNode): PenpotTextNode | undefined {
if (this.isParagraphStarting) {
const isList = segment.listOptions.type !== 'NONE';

return this.segmentParagraphSpacing(
this.isPreviousNodeAList && isList ? node.listSpacing : node.paragraphSpacing
);
}
}

private isFirstTextNode(textNode: PenpotTextNode) {
if (this.firstTextNode === null) {
this.firstTextNode = textNode;
return true;
}

return false;
}

private segmentIndent(indent: number): PenpotTextNode {
return {
text: ' '.repeat(indent),
fontId: 'sourcesanspro',
fontVariantId: 'regular',
fontSize: '5',
fontStyle: 'normal',
fontWeight: '400',
lineHeight: 1,
letterSpacing: 0
};
}

private segmentParagraphSpacing(paragraphSpacing: number): PenpotTextNode | undefined {
if (paragraphSpacing === 0) return;

return {
text: '\n',
fontId: 'sourcesanspro',
fontVariantId: 'regular',
fontSize: paragraphSpacing.toString(),
fontStyle: 'normal',
fontWeight: '400',
lineHeight: 1,
letterSpacing: 0
};
}
}
7 changes: 7 additions & 0 deletions plugin-src/translators/text/paragraph/UnorderedListType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ListType } from './ListType';

export class UnorderedListType implements ListType {
public getCurrentSymbol(_number: number, _indentation: number): string {
return ' • ';
}
}
7 changes: 7 additions & 0 deletions plugin-src/translators/text/paragraph/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export * from './List';
export * from './ListType';
export * from './ListTypeFactory';
export * from './OrderedListType';
export * from './Paragraph';
export * from './translateParagraphProperties';
export * from './UnorderedListType';

0 comments on commit 2920ac2

Please sign in to comment.