canva-pptx 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1147 -0
- package/package.json +63 -0
- package/src/AdvancedComponents/accordionList.js +39 -0
- package/src/AdvancedComponents/calendarGrid.js +50 -0
- package/src/AdvancedComponents/certificateFrame.js +35 -0
- package/src/AdvancedComponents/deviceMockup.js +59 -0
- package/src/AdvancedComponents/featureGrid.js +70 -0
- package/src/AdvancedComponents/funnelDiagram.js +49 -0
- package/src/AdvancedComponents/ganttChart.js +35 -0
- package/src/AdvancedComponents/invoiceTable.js +32 -0
- package/src/AdvancedComponents/matrixGrid.js +43 -0
- package/src/AdvancedComponents/mindMap.js +43 -0
- package/src/AdvancedComponents/orgChart.js +76 -0
- package/src/AdvancedComponents/personaCard.js +44 -0
- package/src/AdvancedComponents/processCycle.js +47 -0
- package/src/AdvancedComponents/pyramidHierarchy.js +38 -0
- package/src/AdvancedComponents/saasFeatureBlock.js +38 -0
- package/src/AdvancedComponents/swotMatrix.js +55 -0
- package/src/AdvancedComponents/teamMemberProfile.js +75 -0
- package/src/AdvancedComponents/trafficLight.js +35 -0
- package/src/AdvancedComponents/vennDiagram.js +35 -0
- package/src/AdvancedComponents/winLossChart.js +43 -0
- package/src/components/StatMetric.js +43 -0
- package/src/components/alertBox.js +46 -0
- package/src/components/avatarGroup.js +56 -0
- package/src/components/badge.js +40 -0
- package/src/components/breadcrumbNav.js +31 -0
- package/src/components/browserWindow.js +86 -0
- package/src/components/brushStroke.js +23 -0
- package/src/components/callToAction.js +27 -0
- package/src/components/card.js +64 -0
- package/src/components/chart.js +19 -0
- package/src/components/codeBlock.js +40 -0
- package/src/components/codeDiff.js +51 -0
- package/src/components/comparisonTable.js +43 -0
- package/src/components/cornerAccent.js +32 -0
- package/src/components/dotPattern.js +30 -0
- package/src/components/geometricConfetti.js +34 -0
- package/src/components/gradientMesh.js +32 -0
- package/src/components/iconList.js +43 -0
- package/src/components/image.js +11 -0
- package/src/components/kanbanColumn.js +38 -0
- package/src/components/link.js +23 -0
- package/src/components/organicBlob.js +45 -0
- package/src/components/pricingColumn.js +53 -0
- package/src/components/progressBar.js +55 -0
- package/src/components/ratingStars.js +25 -0
- package/src/components/shape.js +12 -0
- package/src/components/slide.js +5 -0
- package/src/components/socialBar.js +59 -0
- package/src/components/squiggleLine.js +26 -0
- package/src/components/stepProcess.js +39 -0
- package/src/components/table.js +23 -0
- package/src/components/tagCloud.js +39 -0
- package/src/components/testimonialCard.js +54 -0
- package/src/components/text.js +14 -0
- package/src/components/theme.js +7 -0
- package/src/components/timeline.js +73 -0
- package/src/components/waveDecoration.js +35 -0
- package/src/core/PPTManager.js +17 -0
- package/src/index.js +225 -0
- package/src/layout/bento.js +47 -0
- package/src/layout/checkerboard.js +36 -0
- package/src/layout/filmStrip.js +29 -0
- package/src/layout/gallery.js +50 -0
- package/src/layout/grid.js +37 -0
- package/src/layout/hero.js +30 -0
- package/src/layout/magazine.js +39 -0
- package/src/layout/radial.js +34 -0
- package/src/layout/sidebar.js +26 -0
- package/src/layout/splitScreen.js +36 -0
- package/src/layout/zPattern.js +29 -0
- package/src/system/animation.js +36 -0
- package/src/system/contrastChecker.js +35 -0
- package/src/system/dataAdapter.js +36 -0
- package/src/system/layoutDebugger.js +40 -0
- package/src/system/markdownEngine.js +45 -0
- package/src/system/masterOverlay.js +50 -0
- package/src/system/sectionDivider.js +41 -0
- package/src/system/smartIcon.js +33 -0
- package/src/system/smartText.js +44 -0
- package/src/system/speakerNotes.js +12 -0
- package/src/system/syntaxHighlighter.js +71 -0
- package/src/system/themeGenerator.js +25 -0
- package/src/system/tocGenerator.js +68 -0
- package/src/system/watermark.js +26 -0
- package/src/themes/index.js +93 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns zones for alternating text/image rows.
|
|
3
|
+
* Row 1: Text Left, Image Right
|
|
4
|
+
* Row 2: Image Left, Text Right
|
|
5
|
+
*/
|
|
6
|
+
function getCheckerboardLayout(rows = 2, options = {}) {
|
|
7
|
+
const {
|
|
8
|
+
x = 0.5, y = 0.5, w = 9, h = 4.63,
|
|
9
|
+
gap = 0.5
|
|
10
|
+
} = options;
|
|
11
|
+
|
|
12
|
+
const rowHeight = (h - (rows - 1) * gap) / rows;
|
|
13
|
+
const colWidth = (w - gap) / 2;
|
|
14
|
+
const zones = [];
|
|
15
|
+
|
|
16
|
+
for (let i = 0; i < rows; i++) {
|
|
17
|
+
const isEven = i % 2 === 0;
|
|
18
|
+
const curY = y + i * (rowHeight + gap);
|
|
19
|
+
|
|
20
|
+
if (isEven) {
|
|
21
|
+
zones.push({
|
|
22
|
+
text: { x: x, y: curY, w: colWidth, h: rowHeight },
|
|
23
|
+
media: { x: x + colWidth + gap, y: curY, w: colWidth, h: rowHeight }
|
|
24
|
+
});
|
|
25
|
+
} else {
|
|
26
|
+
zones.push({
|
|
27
|
+
media: { x: x, y: curY, w: colWidth, h: rowHeight },
|
|
28
|
+
text: { x: x + colWidth + gap, y: curY, w: colWidth, h: rowHeight }
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return zones;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { getCheckerboardLayout };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a horizontal strip of items, potentially overflowing the slide width
|
|
3
|
+
* (though in PPT, we clamp or fit them).
|
|
4
|
+
*/
|
|
5
|
+
function getFilmStripLayout(count = 5, options = {}) {
|
|
6
|
+
const {
|
|
7
|
+
x = 0, y = 2, w = 10, h = 1.5,
|
|
8
|
+
gap = 0.2
|
|
9
|
+
} = options;
|
|
10
|
+
|
|
11
|
+
// Calculate item width to fit ALL in the specific width
|
|
12
|
+
const totalGap = (count - 1) * gap;
|
|
13
|
+
const itemW = (w - (x * 2) - totalGap) / count;
|
|
14
|
+
|
|
15
|
+
const positions = [];
|
|
16
|
+
|
|
17
|
+
for (let i = 0; i < count; i++) {
|
|
18
|
+
positions.push({
|
|
19
|
+
x: x + i * (itemW + gap),
|
|
20
|
+
y: y,
|
|
21
|
+
w: itemW,
|
|
22
|
+
h: h
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return positions;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { getFilmStripLayout };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns an array of coordinate objects for N items.
|
|
3
|
+
* Automatically calculates columns based on item count if not specified.
|
|
4
|
+
*/
|
|
5
|
+
function getGalleryLayout(count = 6, options = {}) {
|
|
6
|
+
const {
|
|
7
|
+
x = 0.5,
|
|
8
|
+
y = 0.5,
|
|
9
|
+
w = 9,
|
|
10
|
+
h = 4.63,
|
|
11
|
+
gap = 0.2,
|
|
12
|
+
cols = undefined // if undefined, auto-calculate
|
|
13
|
+
} = options;
|
|
14
|
+
|
|
15
|
+
// Auto-determine columns if not provided
|
|
16
|
+
// 1-3 items = 1 row (count cols)
|
|
17
|
+
// 4 items = 2 cols
|
|
18
|
+
// 5-6 items = 3 cols
|
|
19
|
+
// 7+ items = 4 cols
|
|
20
|
+
let calculatedCols = cols;
|
|
21
|
+
if (!calculatedCols) {
|
|
22
|
+
if (count <= 3) calculatedCols = count;
|
|
23
|
+
else if (count === 4) calculatedCols = 2;
|
|
24
|
+
else if (count <= 6) calculatedCols = 3;
|
|
25
|
+
else calculatedCols = 4;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const rows = Math.ceil(count / calculatedCols);
|
|
29
|
+
|
|
30
|
+
const itemW = (w - (calculatedCols - 1) * gap) / calculatedCols;
|
|
31
|
+
const itemH = (h - (rows - 1) * gap) / rows;
|
|
32
|
+
|
|
33
|
+
const positions = [];
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < count; i++) {
|
|
36
|
+
const colIndex = i % calculatedCols;
|
|
37
|
+
const rowIndex = Math.floor(i / calculatedCols);
|
|
38
|
+
|
|
39
|
+
positions.push({
|
|
40
|
+
x: x + colIndex * (itemW + gap),
|
|
41
|
+
y: y + rowIndex * (itemH + gap),
|
|
42
|
+
w: itemW,
|
|
43
|
+
h: itemH
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return positions;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { getGalleryLayout };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a grid system inside a container
|
|
3
|
+
* Container can be full slide or a subsection
|
|
4
|
+
*/
|
|
5
|
+
function createGrid(container, options = {}) {
|
|
6
|
+
const {
|
|
7
|
+
x = 0,
|
|
8
|
+
y = 0,
|
|
9
|
+
w = 10,
|
|
10
|
+
h = 5.63, // default slide height
|
|
11
|
+
cols = 2,
|
|
12
|
+
rows = 1,
|
|
13
|
+
gap = 0.2,
|
|
14
|
+
} = options;
|
|
15
|
+
|
|
16
|
+
const colWidth = (w - gap * (cols - 1)) / cols;
|
|
17
|
+
const rowHeight = (h - gap * (rows - 1)) / rows;
|
|
18
|
+
|
|
19
|
+
function getCell(col, row) {
|
|
20
|
+
return {
|
|
21
|
+
x: x + col * (colWidth + gap),
|
|
22
|
+
y: y + row * (rowHeight + gap),
|
|
23
|
+
w: colWidth,
|
|
24
|
+
h: rowHeight,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
getCell,
|
|
30
|
+
colWidth,
|
|
31
|
+
rowHeight,
|
|
32
|
+
cols,
|
|
33
|
+
rows,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { createGrid };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns zones for a Title, Subtitle, and Background Image.
|
|
3
|
+
* @param {Object} options - { align: 'center'|'left' }
|
|
4
|
+
*/
|
|
5
|
+
function getHeroLayout(options = {}) {
|
|
6
|
+
const {
|
|
7
|
+
align = "center",
|
|
8
|
+
w = 10,
|
|
9
|
+
h = 5.63,
|
|
10
|
+
padding = 1
|
|
11
|
+
} = options;
|
|
12
|
+
|
|
13
|
+
// Background is always full slide
|
|
14
|
+
const background = { x: 0, y: 0, w: w, h: h };
|
|
15
|
+
|
|
16
|
+
// Content zones based on alignment
|
|
17
|
+
let title, subtitle;
|
|
18
|
+
|
|
19
|
+
if (align === "center") {
|
|
20
|
+
title = { x: 0, y: h * 0.4, w: w, h: 1 };
|
|
21
|
+
subtitle = { x: 0, y: h * 0.4 + 1, w: w, h: 1 };
|
|
22
|
+
} else if (align === "left") {
|
|
23
|
+
title = { x: padding, y: h * 0.4, w: w - padding * 2, h: 1 };
|
|
24
|
+
subtitle = { x: padding, y: h * 0.4 + 1, w: w - padding * 2, h: 1 };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return { background, title, subtitle };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { getHeroLayout };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a magazine-style layout: One main focus item, multiple side items.
|
|
3
|
+
*/
|
|
4
|
+
function getMagazineLayout(sideItemCount = 3, options = {}) {
|
|
5
|
+
const {
|
|
6
|
+
x = 0.5, y = 0.5, w = 9, h = 4.63,
|
|
7
|
+
gap = 0.2,
|
|
8
|
+
focusPosition = "left" // 'left' or 'right'
|
|
9
|
+
} = options;
|
|
10
|
+
|
|
11
|
+
// Focus takes up 60% width
|
|
12
|
+
const focusW = w * 0.6 - gap;
|
|
13
|
+
const sideW = w * 0.4;
|
|
14
|
+
|
|
15
|
+
const sideItemH = (h - (sideItemCount - 1) * gap) / sideItemCount;
|
|
16
|
+
|
|
17
|
+
const focusZone = {
|
|
18
|
+
x: focusPosition === "left" ? x : x + sideW + gap,
|
|
19
|
+
y: y,
|
|
20
|
+
w: focusW,
|
|
21
|
+
h: h
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const sideZones = [];
|
|
25
|
+
const sideX = focusPosition === "left" ? x + focusW + gap : x;
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < sideItemCount; i++) {
|
|
28
|
+
sideZones.push({
|
|
29
|
+
x: sideX,
|
|
30
|
+
y: y + i * (sideItemH + gap),
|
|
31
|
+
w: sideW,
|
|
32
|
+
h: sideItemH
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { focus: focusZone, side: sideZones };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { getMagazineLayout };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns coordinates for N items arranged in a circle.
|
|
3
|
+
* @param {number} count - Number of items
|
|
4
|
+
*/
|
|
5
|
+
function getRadialLayout(count = 5, options = {}) {
|
|
6
|
+
const {
|
|
7
|
+
x = 5, y = 2.8, // Center of slide
|
|
8
|
+
radius = 2,
|
|
9
|
+
itemSize = 1
|
|
10
|
+
} = options;
|
|
11
|
+
|
|
12
|
+
const positions = [];
|
|
13
|
+
const angleStep = (2 * Math.PI) / count;
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i < count; i++) {
|
|
16
|
+
// Start from top (-PI/2)
|
|
17
|
+
const angle = i * angleStep - Math.PI / 2;
|
|
18
|
+
|
|
19
|
+
const curX = x + radius * Math.cos(angle) - (itemSize / 2);
|
|
20
|
+
const curY = y + radius * Math.sin(angle) - (itemSize / 2);
|
|
21
|
+
|
|
22
|
+
positions.push({
|
|
23
|
+
x: curX,
|
|
24
|
+
y: curY,
|
|
25
|
+
w: itemSize,
|
|
26
|
+
h: itemSize,
|
|
27
|
+
angle: (angle * 180) / Math.PI // Return angle in degrees for rotation if needed
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return positions;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { getRadialLayout };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns coordinates for a sidebar layout.
|
|
3
|
+
* @param {Object} options - { position: 'left'|'right', width: 2.5 }
|
|
4
|
+
*/
|
|
5
|
+
function getSidebarLayout(options = {}) {
|
|
6
|
+
const {
|
|
7
|
+
position = "left",
|
|
8
|
+
width = 2.5,
|
|
9
|
+
w = 10,
|
|
10
|
+
h = 5.63
|
|
11
|
+
} = options;
|
|
12
|
+
|
|
13
|
+
if (position === "left") {
|
|
14
|
+
return {
|
|
15
|
+
sidebar: { x: 0, y: 0, w: width, h: h },
|
|
16
|
+
main: { x: width, y: 0, w: w - width, h: h }
|
|
17
|
+
};
|
|
18
|
+
} else {
|
|
19
|
+
return {
|
|
20
|
+
main: { x: 0, y: 0, w: w - width, h: h },
|
|
21
|
+
sidebar: { x: w - width, y: 0, w: width, h: h }
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { getSidebarLayout };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns coordinates for a split screen layout.
|
|
3
|
+
* @param {Object} options - { type: 'vertical'|'horizontal', ratio: 0.5, gutter: 0.2 }
|
|
4
|
+
*/
|
|
5
|
+
function getSplitScreen(options = {}) {
|
|
6
|
+
const {
|
|
7
|
+
type = "vertical", // 'vertical' (left/right) or 'horizontal' (top/bottom)
|
|
8
|
+
ratio = 0.5, // Split ratio (0.5 = 50/50)
|
|
9
|
+
gutter = 0, // Space between panels
|
|
10
|
+
w = 10,
|
|
11
|
+
h = 5.63,
|
|
12
|
+
x = 0,
|
|
13
|
+
y = 0
|
|
14
|
+
} = options;
|
|
15
|
+
|
|
16
|
+
if (type === "vertical") {
|
|
17
|
+
const width1 = (w * ratio) - (gutter / 2);
|
|
18
|
+
const width2 = (w * (1 - ratio)) - (gutter / 2);
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
left: { x: x, y: y, w: width1, h: h },
|
|
22
|
+
right: { x: x + width1 + gutter, y: y, w: width2, h: h }
|
|
23
|
+
};
|
|
24
|
+
} else {
|
|
25
|
+
// Horizontal (Top/Bottom)
|
|
26
|
+
const height1 = (h * ratio) - (gutter / 2);
|
|
27
|
+
const height2 = (h * (1 - ratio)) - (gutter / 2);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
top: { x: x, y: y, w: w, h: height1 },
|
|
31
|
+
bottom: { x: x, y: y + height1 + gutter, w: w, h: height2 }
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { getSplitScreen };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns 4 coordinate zones in a Z-pattern.
|
|
3
|
+
* Useful for guiding the eye through 4 key points.
|
|
4
|
+
*/
|
|
5
|
+
function getZPattern(options = {}) {
|
|
6
|
+
const {
|
|
7
|
+
w = 10,
|
|
8
|
+
h = 5.63,
|
|
9
|
+
margin = 0.5,
|
|
10
|
+
gap = 0.4
|
|
11
|
+
} = options;
|
|
12
|
+
|
|
13
|
+
// Workable area
|
|
14
|
+
const safeW = w - (margin * 2);
|
|
15
|
+
const safeH = h - (margin * 2);
|
|
16
|
+
|
|
17
|
+
// Dimensions for each quadrant
|
|
18
|
+
const itemW = (safeW - gap) / 2;
|
|
19
|
+
const itemH = (safeH - gap) / 2;
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
topLeft: { x: margin, y: margin, w: itemW, h: itemH },
|
|
23
|
+
topRight: { x: margin + itemW + gap, y: margin, w: itemW, h: itemH },
|
|
24
|
+
bottomLeft: { x: margin, y: margin + itemH + gap, w: itemW, h: itemH },
|
|
25
|
+
bottomRight: { x: margin + itemW + gap, y: margin + itemH + gap, w: itemW, h: itemH }
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { getZPattern };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animation helper.
|
|
3
|
+
* Usage: animate(slide).addText(...).withEffect('fadeIn')
|
|
4
|
+
*/
|
|
5
|
+
class AnimationBuilder {
|
|
6
|
+
constructor(slide) {
|
|
7
|
+
this.slide = slide;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
addShape(type, options) {
|
|
11
|
+
this.lastShape = this.slide.addShape(type, options);
|
|
12
|
+
return this;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
addText(text, options) {
|
|
16
|
+
this.lastShape = this.slide.addText(text, options);
|
|
17
|
+
return this;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Chainable animation
|
|
21
|
+
withEffect(effectType = "fadeIn", duration = 1000) {
|
|
22
|
+
// Note: pptxgenjs usually applies animation in the options object of addShape/addText.
|
|
23
|
+
// If the object was already created, modifying it is hard.
|
|
24
|
+
// A true sequencer usually wraps the *creation*.
|
|
25
|
+
// Here we assume the user calls this wrapper to generate the options.
|
|
26
|
+
console.log("Animation metadata attached (Concept only in this wrapper)");
|
|
27
|
+
// In real implementation, we'd merge { animation: { type: effectType } } into options
|
|
28
|
+
return this;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function animate(slide) {
|
|
33
|
+
return new AnimationBuilder(slide);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { animate };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks contrast ratio between text and background.
|
|
3
|
+
* Logs a warning if ratio is below 4.5:1 (WCAG AA).
|
|
4
|
+
* * Note: This runs in the console during generation, not on the slide itself.
|
|
5
|
+
*/
|
|
6
|
+
function checkContrast(slideName, textColorHex, bgColorHex) {
|
|
7
|
+
// Helper to get luminance
|
|
8
|
+
const getLum = (hex) => {
|
|
9
|
+
const rgb = parseInt(hex.replace("#", ""), 16);
|
|
10
|
+
const r = (rgb >> 16) & 0xff;
|
|
11
|
+
const g = (rgb >> 8) & 0xff;
|
|
12
|
+
const b = (rgb >> 0) & 0xff;
|
|
13
|
+
|
|
14
|
+
// WCAG formula
|
|
15
|
+
const a = [r, g, b].map(v => {
|
|
16
|
+
v /= 255;
|
|
17
|
+
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
18
|
+
});
|
|
19
|
+
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const lum1 = getLum(textColorHex || "000000");
|
|
23
|
+
const lum2 = getLum(bgColorHex || "FFFFFF");
|
|
24
|
+
const brightest = Math.max(lum1, lum2);
|
|
25
|
+
const darkest = Math.min(lum1, lum2);
|
|
26
|
+
const ratio = (brightest + 0.05) / (darkest + 0.05);
|
|
27
|
+
|
|
28
|
+
if (ratio < 4.5) {
|
|
29
|
+
console.warn(`⚠️ ACCESSIBILITY WARN [${slideName}]: Low contrast ratio (${ratio.toFixed(2)}). Text: #${textColorHex} on Bg: #${bgColorHex}`);
|
|
30
|
+
} else {
|
|
31
|
+
// console.log(`✅ Contrast OK: ${ratio.toFixed(2)}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { checkContrast };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts array of objects to Table rows.
|
|
3
|
+
* Input: [{ id: 1, name: 'A'}, { id: 2, name: 'B' }]
|
|
4
|
+
* Output: [['id', 'name'], [1, 'A'], [2, 'B']]
|
|
5
|
+
*/
|
|
6
|
+
function jsonToTableData(jsonArray) {
|
|
7
|
+
if (!jsonArray || jsonArray.length === 0) return [];
|
|
8
|
+
|
|
9
|
+
const headers = Object.keys(jsonArray[0]);
|
|
10
|
+
const rows = jsonArray.map(obj => Object.values(obj));
|
|
11
|
+
|
|
12
|
+
return [headers, ...rows];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Converts CSV string to Table rows.
|
|
17
|
+
*/
|
|
18
|
+
function csvToTableData(csvString) {
|
|
19
|
+
const rows = csvString.trim().split("\n");
|
|
20
|
+
return rows.map(r => r.split(",").map(c => c.trim()));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Converts JSON to Chart Data format for pptxgenjs.
|
|
25
|
+
* Input: [{ label: "Q1", value: 10 }, { label: "Q2", value: 20 }]
|
|
26
|
+
* Output: [{ name: "Series1", labels: ["Q1", "Q2"], values: [10, 20] }]
|
|
27
|
+
*/
|
|
28
|
+
function jsonToChartData(jsonArray, labelKey, valueKey, seriesName = "Series 1") {
|
|
29
|
+
return [{
|
|
30
|
+
name: seriesName,
|
|
31
|
+
labels: jsonArray.map(i => i[labelKey]),
|
|
32
|
+
values: jsonArray.map(i => i[valueKey])
|
|
33
|
+
}];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { jsonToTableData, csvToTableData, jsonToChartData };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visual Debugger
|
|
3
|
+
* Draws a 1-inch grid and safe-zone borders to help align content.
|
|
4
|
+
*/
|
|
5
|
+
function debugSlide(slide, options = {}) {
|
|
6
|
+
const { color = "FF0000", showGrid = true, showSafeZone = true } = options;
|
|
7
|
+
|
|
8
|
+
// 1. Safe Zone Border (Margin)
|
|
9
|
+
if (showSafeZone) {
|
|
10
|
+
slide.addShape("rect", {
|
|
11
|
+
x: 0.5, y: 0.5, w: 9, h: 4.63,
|
|
12
|
+
fill: { color: undefined },
|
|
13
|
+
line: { color: color, width: 1, dashType: "dash" }
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 2. Grid Lines (Every 1 inch)
|
|
18
|
+
if (showGrid) {
|
|
19
|
+
// Vertical Lines (0 to 10)
|
|
20
|
+
for (let i = 1; i < 10; i++) {
|
|
21
|
+
slide.addShape("line", {
|
|
22
|
+
x: i, y: 0, w: 0, h: 5.63,
|
|
23
|
+
line: { color: "E0E0E0", width: 1 } // Light grey
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
// Horizontal Lines (0 to 5.63)
|
|
27
|
+
for (let i = 1; i < 6; i++) {
|
|
28
|
+
slide.addShape("line", {
|
|
29
|
+
x: 0, y: i, w: 10, h: 0,
|
|
30
|
+
line: { color: "E0E0E0", width: 1 }
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 3. Center Crosshair
|
|
36
|
+
slide.addShape("line", { x: 5, y: 2.7, w: 0, h: 0.2, line: { color: color } }); // V
|
|
37
|
+
slide.addShape("line", { x: 4.9, y: 2.815, w: 0.2, h: 0, line: { color: color } }); // H
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { debugSlide };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const { addSlide } = require("../components/slide");
|
|
2
|
+
const { addText } = require("../components/text");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parses markdown string and generates slides.
|
|
6
|
+
* Separator: "---" creates a new slide.
|
|
7
|
+
* # Title
|
|
8
|
+
* ## Subtitle
|
|
9
|
+
* - Bullet point
|
|
10
|
+
*/
|
|
11
|
+
function renderMarkdown(manager, markdown) {
|
|
12
|
+
const pptx = manager.getPptx();
|
|
13
|
+
const slides = markdown.split(/\n---\n/); // Split by horizontal rule
|
|
14
|
+
|
|
15
|
+
slides.forEach(content => {
|
|
16
|
+
const slide = addSlide(pptx);
|
|
17
|
+
const lines = content.trim().split("\n");
|
|
18
|
+
let yPos = 1.0;
|
|
19
|
+
|
|
20
|
+
lines.forEach(line => {
|
|
21
|
+
line = line.trim();
|
|
22
|
+
if (!line) return;
|
|
23
|
+
|
|
24
|
+
if (line.startsWith("# ")) {
|
|
25
|
+
// H1 Title
|
|
26
|
+
addText(slide, line.replace("# ", ""), { x: 0.5, y: 0.5, fontSize: 32, bold: true, w: 9 });
|
|
27
|
+
yPos = 1.5;
|
|
28
|
+
} else if (line.startsWith("## ")) {
|
|
29
|
+
// H2 Subtitle
|
|
30
|
+
addText(slide, line.replace("## ", ""), { x: 0.5, y: yPos, fontSize: 20, color: "666666", w: 9 });
|
|
31
|
+
yPos += 0.8;
|
|
32
|
+
} else if (line.startsWith("- ")) {
|
|
33
|
+
// Bullet
|
|
34
|
+
addText(slide, line.replace("- ", ""), { x: 1, y: yPos, fontSize: 16, bullet: true, w: 8, h: 0.5 });
|
|
35
|
+
yPos += 0.6;
|
|
36
|
+
} else {
|
|
37
|
+
// Regular Paragraph
|
|
38
|
+
addText(slide, line, { x: 1, y: yPos, fontSize: 14, w: 8 });
|
|
39
|
+
yPos += 0.6;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { renderMarkdown };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies a layout master function to a slide.
|
|
3
|
+
* Useful for logos, footers, and page numbers.
|
|
4
|
+
*/
|
|
5
|
+
function applyMaster(slide, options = {}) {
|
|
6
|
+
const {
|
|
7
|
+
logo,
|
|
8
|
+
footerText,
|
|
9
|
+
showPageNumber = true,
|
|
10
|
+
accentColor = "007AFF"
|
|
11
|
+
} = options;
|
|
12
|
+
|
|
13
|
+
// 1. Logo (Top Right)
|
|
14
|
+
if (logo) {
|
|
15
|
+
slide.addImage({
|
|
16
|
+
path: logo,
|
|
17
|
+
x: 9.0, y: 0.2, w: 0.8, h: 0.8,
|
|
18
|
+
sizing: { type: "contain", w: 0.8, h: 0.8 }
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 2. Footer Bar (Bottom)
|
|
23
|
+
slide.addShape("line", {
|
|
24
|
+
x: 0.5, y: 5.2, w: 9, h: 0,
|
|
25
|
+
line: { color: "E0E0E0", width: 1 }
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// 3. Footer Text (Bottom Left)
|
|
29
|
+
if (footerText) {
|
|
30
|
+
slide.addText(footerText, {
|
|
31
|
+
x: 0.5, y: 5.3, w: 6, h: 0.3,
|
|
32
|
+
fontSize: 10, color: "888888"
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 4. Page Number (Bottom Right)
|
|
37
|
+
if (showPageNumber) {
|
|
38
|
+
// FIX: slideNumber is a property, not a function
|
|
39
|
+
slide.slideNumber = {
|
|
40
|
+
x: 9.0,
|
|
41
|
+
y: 5.3,
|
|
42
|
+
w: 0.5,
|
|
43
|
+
h: 0.3,
|
|
44
|
+
fontSize: 10,
|
|
45
|
+
color: accentColor
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { applyMaster };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a distinct section divider slide.
|
|
3
|
+
* Useful for breaking up long presentations.
|
|
4
|
+
*/
|
|
5
|
+
function addSectionDivider(manager, title, subtitle = "") {
|
|
6
|
+
const pptx = manager.getPptx();
|
|
7
|
+
const slide = pptx.addSlide();
|
|
8
|
+
|
|
9
|
+
// Dark Background for contrast
|
|
10
|
+
slide.background = { fill: "111111" };
|
|
11
|
+
|
|
12
|
+
// Big Title
|
|
13
|
+
slide.addText(title, {
|
|
14
|
+
x: 1, y: 2, w: 8, h: 1,
|
|
15
|
+
fontSize: 44,
|
|
16
|
+
bold: true,
|
|
17
|
+
color: "FFFFFF",
|
|
18
|
+
align: "center"
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Horizontal Line
|
|
22
|
+
slide.addShape("line", {
|
|
23
|
+
x: 3, y: 3.2, w: 4, h: 0,
|
|
24
|
+
line: { color: "007AFF", width: 3 }
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Subtitle
|
|
28
|
+
if (subtitle) {
|
|
29
|
+
slide.addText(subtitle, {
|
|
30
|
+
x: 1, y: 3.5, w: 8, h: 1,
|
|
31
|
+
fontSize: 18,
|
|
32
|
+
color: "BBBBBB",
|
|
33
|
+
align: "center",
|
|
34
|
+
italic: true
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return slide;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { addSectionDivider };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// A small set of standard SVG paths (simulated)
|
|
2
|
+
const ICONS = {
|
|
3
|
+
user: "M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z",
|
|
4
|
+
home: "M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z",
|
|
5
|
+
settings: "M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.68 8.87c-.11.21-.06.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.04.24.24.41.48.41h3.84c.24 0.43-.17.47-.41l.36-2.54c.59-.24 1.13-.57 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.11-.21.06-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z",
|
|
6
|
+
star: "M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function addSmartIcon(slide, iconName, options = {}) {
|
|
10
|
+
const { x = 1, y = 1, size = 0.5, color = "000000" } = options;
|
|
11
|
+
|
|
12
|
+
// Note: pptxgenjs supports SVG paths via 'path' property in addShape (custom geometry)
|
|
13
|
+
// or via addImage with dataURI. Custom geometry is cleaner if supported,
|
|
14
|
+
// but addImage with SVG XML is more robust across versions.
|
|
15
|
+
|
|
16
|
+
const pathData = ICONS[iconName];
|
|
17
|
+
if (!pathData) {
|
|
18
|
+
console.warn(`Icon ${iconName} not found.`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Construct a minimal SVG string
|
|
23
|
+
const svgData = `data:image/svg+xml;base64,${Buffer.from(
|
|
24
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#${color}"><path d="${pathData}"/></svg>`
|
|
25
|
+
).toString("base64")}`;
|
|
26
|
+
|
|
27
|
+
slide.addImage({
|
|
28
|
+
data: svgData,
|
|
29
|
+
x, y, w: size, h: size
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { addSmartIcon };
|