Skip to content

Instantly share code, notes, and snippets.

@josephxbrick
Last active April 24, 2021 23:42
Show Gist options
  • Save josephxbrick/9ebf2af1bf4132611574db4c90a8659c to your computer and use it in GitHub Desktop.
Save josephxbrick/9ebf2af1bf4132611574db4c90a8659c to your computer and use it in GitHub Desktop.
/* createTOC */
// This script updates a table of contents for your document. It assumes your document is using
// sections, as each section will be an entry in the TOC
// Please view the "sample" page for this script in the file
// This script also adds section numbers to section and page headers and names the docPage's frame
// Along the way it sorts the docPages and page-numbers them, using other scripts in this file.
// See the other scripts for code comments on them
// We will use an array of this type to collect information about the document as we go.
interface DocPageInfo {
docPage:FrameNode;
pageNumber:number;
sectionNumber:number;
pageInSectionNumber:number;
pageTitle:string;
pageType: "page"|"section"|"";
}
const createTOC = (docInfo:DocPageInfo[]) =>{
let tocItemsCreated:number = 0;
const tocSections = docInfo.filter(node => node.pageType === 'section');
if (tocSections.length === 0){
return;
}
// find TOC frame
let tocFrame:FrameNode|null = null;
for (let i = 0; i < docInfo.length; i++){
tocFrame = docInfo[i].docPage.findChild(node => node.type === 'FRAME' && node.name === '#tocFrame') as FrameNode|null;
if (tocFrame !== null){
break
}
}
if (tocFrame === null){
return;
}
const tocItems = tocFrame.findChildren(node => node.type === 'INSTANCE' && node.name === '#tocItem');
let length:number = tocItems.length;
if (length === 0){
return 0;
}
// remove all but one item
for (let i = 0; i < length - 1 ;i++){
const itemToDelete = tocItems.pop() as InstanceNode;
itemToDelete.remove();
}
const cloner = tocItems[0] as InstanceNode;
for (let i = 0;i < tocSections.length - 1; i++){
const clone = cloner.clone()
tocFrame.appendChild(clone);
tocItems.push(clone);
}
for (let i = 0; i < tocItems.length; i++){
const tocItem = tocItems[i] as InstanceNode;
setTextOfContainer(tocItem, '#tocPageNum', tocSections[i].pageNumber.toString());
setTextOfContainer(tocItem, '#tocPageName', `${tocSections[i].sectionNumber}. ${tocSections[i].pageTitle}`);
}
figma.notify(`${tocItems.length} TOC items created; ${docInfo.length} pages processed.`,{timeout: 4000});
return tocItems;
}
interface Options {
hSpacing: number; // horizontal spacing between frames
vSpacing: number; // vertical spacing between frames
vSpacingWithinSection:number; // vertical spacing between frames when a section wraps
}
const sortedDocPages = (options:Options) => {
// insure that spacing values are 0 or greater
options.hSpacing = Math.max(options.hSpacing, 0);
options.vSpacing = Math.max(options.vSpacing, 0);
// inner function adjusts the in-between spacing and aligns the tops of frames of the given row.
// It returns the y value of the bottom of the row (i.e., the bottom of its tallest frame)
const tidyUpRow = (rowFrames:FrameNode[], rowX:number, rowY:number, rowSpacing:number) => {
let maxFrameHeight = 0;
for (const frame of rowFrames) {
maxFrameHeight = Math.max(frame.height, maxFrameHeight);
frame.x = rowX;
frame.y = rowY;
// update rowX to the desired x value of next frame in the row
rowX += frame.width + rowSpacing;
}
return rowY + maxFrameHeight;
}
let sortedFrames:FrameNode[] = [];
let rowX = 0;
let rowY = 0;
let isFirstRow = true;
let spacing = 0;
// get all top-level frames on the page
let availableFrames = figma.currentPage.findChildren(n => n.type === 'FRAME') as FrameNode[];
while(availableFrames.length > 0){
// find y of topmost frame in availableFrames, as well as the height of the shortest frame.
// We will use these to find frames in the top row.
let minY = Number.MAX_SAFE_INTEGER;
let minHeight = Number.MAX_SAFE_INTEGER;
for (const frame of availableFrames){
minY = Math.min(minY, frame.y)
minHeight = Math.min(minHeight, frame.height);
}
// find frames in top row: assume a frame is in the top row when its distance from minY
// is less than the height of the shortest frame.
const topRow:FrameNode[] = [];
const otherRows:FrameNode[] = [];
for (const frame of availableFrames){
if (Math.abs(minY - frame.y) < minHeight){
topRow.push(frame)
} else {
otherRows.push(frame)
}
}
// we have the top row of the remaining rows!
topRow.sort((a,b) => a.x - b.x); // sort frames in array by left-to-right positon
// check if this is the first row so we can initialize rowX and rowY
if (isFirstRow){
rowX = topRow[0].x;
rowY = topRow[0].y;
isFirstRow = false;
}
if (topRow[0].findChild(n => n.type === "INSTANCE" && n.name === '#pageInSectionHeader')){
spacing = options.vSpacingWithinSection;
} else {
spacing = options.vSpacing;
}
// clean up the row layout, get y value for top of next row
rowY = tidyUpRow(topRow, rowX, rowY + spacing - options.vSpacing, options.hSpacing) + options.vSpacing;
sortedFrames = [...sortedFrames, ...topRow] // append frames in row to sorted frames
availableFrames = [...otherRows] // update to contain the frames not in any previous row
}
// sort the frames in the layer list such that they read top-to-bottom (i.e., reverse z-order)
for (let i = sortedFrames.length - 1; i >= 0; i--) {
const docPage = sortedFrames[i];
figma.currentPage.appendChild(docPage);
}
// return array of sorted frames
return sortedFrames;
}
// adds page numbers to pages
const numberPages = (sortedDocPages:FrameNode[]) => {
const footerName:string = '#footer';
const textNodeName:string = '#pageNumber';
const docPageInfo:DocPageInfo[] = [];
let curPageNum:number = 0;
for (const docPage of sortedDocPages) {
curPageNum++;
docPageInfo.push({
docPage: docPage,
pageNumber: curPageNum,
} as DocPageInfo)
const footer = docPage.findChild(n => n.type === 'INSTANCE' && n.name === footerName) as InstanceNode|null;
if (footer !== null) {
setTextOfContainer(footer, textNodeName, curPageNum.toString());
}
}
return docPageInfo;
}
// Here's the numberSections code. Its parameter is the return value from numberPages()
const numberSections = (docPageInfo:DocPageInfo[]) => {
let sectionNumber = 0;
let pageInSectionNumber = 0;
let pageTitle:string;
let pageType: "page"|"section"|"titlePage"|"";
for (let i=0; i < docPageInfo.length; i++){
const pageInfo = docPageInfo[i];
pageTitle = '';
pageType = '';
// see if there is an instance of the component #sectionHeader on this page.
// if so, update its section number
let header = pageInfo.docPage.findChild(n => n.type === 'INSTANCE' && n.name === '#sectionHeader') as InstanceNode|null;
if (header !== null) {
sectionNumber++;
pageInSectionNumber = 1;
setTextOfContainer(header, '#sectionNumber', sectionNumber.toString());
pageTitle = getTextOfContainer(header, '#headerTitle')
pageInfo.docPage.name = `${sectionNumber}${pageTitle}`
pageType = "section";
}
if (pageInSectionNumber > 0 && pageType == ''){
// see if there is instance of the component #sectionHeader on this pageInfo.
// if so update its section number
header = pageInfo.docPage.findChild(n => n.type === 'INSTANCE' && n.name === '#pageInSectionHeader') as InstanceNode|null;
if (header !== null) {
setTextOfContainer(header, '#sectionNumber', `${sectionNumber}.${pageInSectionNumber}`)
pageTitle = getTextOfContainer(header, '#headerTitle')
pageType = "page";
pageInfo.docPage.name = `${sectionNumber}.${pageInSectionNumber}${pageTitle}`
pageInSectionNumber++;
}
}
// merge new values into array element's object
docPageInfo[i] = {...docPageInfo[i],...{
pageType:pageType,
sectionNumber: sectionNumber,
pageInSectionNumber: pageInSectionNumber,
pageTitle:pageTitle,
}
}
}
return docPageInfo;
}
// Utility function: sets text of a textnode (of a given name) within a Figma node
// that can contain children (frame, Instance, group, etc.).
// Returns the textNode, or null if no such node exists within the instance
const setTextOfContainer = (container:ChildrenMixin, textNodeName: string, value: string) => {
const textNode = container.findOne(n => n.type === 'TEXT' && n.name === textNodeName) as TextNode|null;
if (textNode !== null) {
textNode.characters = value;
}
return textNode;
}
// Same as setTextOfContainer above, except that it gets the value
const getTextOfContainer = (container:ChildrenMixin, textNodeName: string) => {
const textNode = container.findOne(n => n.type === 'TEXT' && n.name === textNodeName) as TextNode|null;
if (textNode !== null) {
return textNode.characters;
}
return '';
}
// sample code
// get sorted doc pages
// NOTE: Change the spacing values in this row to adjust frame spacing
const pages = sortedDocPages({hSpacing: 100, vSpacing: 500, vSpacingWithinSection: 150 });
// number them and receive an array of type docPageInfo[]
const numberedPages = numberPages(pages);
// number sections and receive an array of type docPageInfo[]
const sectionPages = numberSections(numberedPages);
// create table of contents
createTOC(sectionPages);
console.log(sectionPages);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment