' +
generateTextFrameHtml(obj.paragraphs, baseStyle, pgStyles, charStyles) + '\r\t\t\r';
});
var allStyles = pgStyles.concat(charStyles);
var cssBlocks = map(allStyles, function(obj) {
return formatCssRule('.' + obj.classname, obj.style);
});
if (divs.length > 0) {
cssBlocks.unshift(formatCssRule('p', baseStyle));
}
return {
styles: cssBlocks,
html: divs.join('')
};
}
// Compute the base paragraph style by finding the most common style in frameData
// Side effect: adds cssStyle object alongside each aiStyle object
// frameData: Array of data objects parsed from a collection of TextFrames
// Returns object containing css text style properties of base pg style
function deriveTextStyleCss(frameData) {
var pgStyles = [];
var baseStyle = {};
// override detected settings with these style properties
var defaultCssStyle = {
'text-align': 'left',
'text-transform': 'none',
'padding-bottom': 0,
'padding-top': 0,
'mix-blend-mode': 'normal',
'font-style': 'normal',
'font-weight': 'regular',
'height': 'auto',
'opacity': 1,
'position': 'static' // 'relative' also used (to correct baseline misalignment)
};
var currCharStyles;
forEach(frameData, function(frame) {
forEach(frame.paragraphs, analyzeParagraphStyle);
});
// initialize the base style to be equal to the most common pg style
if (pgStyles.length > 0) {
pgStyles.sort(compareCharCount);
extend(baseStyle, pgStyles[0].cssStyle);
}
// override certain base style properties with default values
extend(baseStyle, defaultCssStyle);
return baseStyle;
function compareCharCount(a, b) {
return b.count - a.count;
}
function analyzeParagraphStyle(pdata) {
currCharStyles = [];
forEach(pdata.ranges, convertRangeStyle);
if (currCharStyles.length > 0) {
// add most common char style to the pg style, to avoid applying
// tags to all the text in the paragraph
currCharStyles.sort(compareCharCount);
extend(pdata.aiStyle, currCharStyles[0].aiStyle);
}
pdata.cssStyle = analyzeTextStyle(pdata.aiStyle, pdata.text, pgStyles);
if (pdata.aiStyle.blendMode && !pdata.cssStyle['mix-blend-mode']) {
warnOnce('Missing a rule for converting ' + pdata.aiStyle.blendMode + ' to CSS.');
}
}
function convertRangeStyle(range) {
range.cssStyle = analyzeTextStyle(range.aiStyle, range.text, currCharStyles);
if (range.warning) {
warn(range.warning.replace('%s', truncateString(range.text, 35)));
}
if (range.aiStyle.aifont && !range.cssStyle['font-family']) {
warnOnce('Missing a rule for converting font: ' + range.aiStyle.aifont +
'. Sample text: ' + truncateString(range.text, 35), range.aiStyle.aifont);
}
}
function analyzeTextStyle(aiStyle, text, stylesArr) {
var cssStyle = convertAiTextStyle(aiStyle);
var key = getStyleKey(cssStyle);
var o;
if (text.length === 0) {
return {};
}
for (var i=0; i -1) {
info.style = 'italic';
}
if (aifont.indexOf('Bold') > -1) {
info.weight = 700;
} else {
info.weight = 500;
}
}
return info;
}
// ai: AI justification value
function getJustificationCss(ai) {
for (var k=0; k 0) {
cssStyle['padding-top'] = aiStyle.spaceBefore + 'px';
}
if (aiStyle.spaceAfter > 0) {
cssStyle['padding-bottom'] = aiStyle.spaceAfter + 'px';
}
if ('tracking' in aiStyle) {
cssStyle['letter-spacing'] = roundTo(aiStyle.tracking / 1000, cssPrecision) + 'em';
}
if (aiStyle.superscript) {
fontSize = roundTo(fontSize * 0.7, 1);
cssStyle['vertical-align'] = 'super';
}
if (aiStyle.subscript) {
fontSize = roundTo(fontSize * 0.7, 1);
cssStyle['vertical-align'] = 'sub';
}
if (fontSize > 0) {
cssStyle['font-size'] = fontSize + 'px';
}
// kludge: text-align of rotated text is handled as a special case (see also getTextFrameCss())
if (aiStyle.rotated && aiStyle.frameType == 'point') {
cssStyle['text-align'] = 'center';
} else if (aiStyle.justification && (tmp = getJustificationCss(aiStyle.justification))) {
cssStyle['text-align'] = tmp;
}
if (aiStyle.capitalization && (tmp = getCapitalizationCss(aiStyle.capitalization))) {
cssStyle['text-transform'] = tmp;
}
if (aiStyle.color) {
cssStyle.color = aiStyle.color;
}
// applying vshift only to point text
// (based on experience with NYTFranklin)
if (aiStyle.size > 0 && fontInfo.vshift && aiStyle.frameType == 'point') {
cssStyle.top = vshiftToPixels(fontInfo.vshift, aiStyle.size);
cssStyle.position = 'relative';
}
return cssStyle;
}
function vshiftToPixels(vshift, fontSize) {
var i = vshift.indexOf('%');
var pct = parseFloat(vshift);
var px = fontSize * pct / 100;
if (!px || i==-1) return '0';
return roundTo(px, 1) + 'px';
}
function textFrameIsRenderable(frame, artboardRect) {
var good = true;
if (!testBoundsIntersection(frame.visibleBounds, artboardRect)) {
good = false;
} else if (frame.kind != TextType.AREATEXT && frame.kind != TextType.POINTTEXT) {
good = false;
} else if (objectIsHidden(frame)) {
good = false;
} else if (frame.contents === '') {
good = false;
}
return good;
}
// Find clipped art objects that are inside an artboard but outside the bounding box
// box of their clipping path
// items: array of PageItems assocated with a clipping path
// clipRect: bounding box of clipping path
// abRect: bounds of artboard to test
//
function selectMaskedItems(items, clipRect, abRect) {
var found = [];
var itemRect, itemInArtboard, itemInMask, maskInArtboard;
for (var i=0, n=items.length; i 0) {
frames = frames.concat(texts);
}
});
return frames;
}
// Get array of TextFrames belonging to an artboard, excluding text that
// overlaps the artboard but is hidden by a clipping mask
function getTextFramesByArtboard(ab, masks, settings) {
var candidateFrames = findTextFramesToRender(doc.textFrames, ab.artboardRect);
var excludedFrames = getClippedTextFramesByArtboard(ab, masks);
candidateFrames = arraySubtract(candidateFrames, excludedFrames);
if (settings.render_rotated_skewed_text_as == 'image') {
excludedFrames = filter(candidateFrames, textIsRotated);
candidateFrames = arraySubtract(candidateFrames, excludedFrames);
}
return candidateFrames;
}
function findTextFramesToRender(frames, artboardRect) {
var selected = [];
for (var i=0; i.convertAreaObjectToPointObject()
// worked correctly (throws MRAP error when trying to remove a converted object)
var textWidth = (bnds[2] - bnds[0]);
copy.transform(matrix);
// Transforming outlines avoids the offcenter problem, but width of bounding
// box needs to be set to width of transformed TextFrame for correct output
copy = copy.createOutline();
copy.transform(app.invertMatrix(matrix));
bnds = copy.geometricBounds;
var dx = Math.ceil(textWidth - (bnds[2] - bnds[0])) / 2;
bnds[0] -= dx;
bnds[2] += dx;
}
copy.remove();
return bnds;
}
function getTransformationCss(textFrame, vertAnchorPct) {
var matrix = clearMatrixShift(textFrame.matrix);
var horizAnchorPct = 50;
var transformOrigin = horizAnchorPct + '% ' + vertAnchorPct + '%;';
var transform = 'matrix(' +
roundTo(matrix.mValueA, cssPrecision) + ',' +
roundTo(-matrix.mValueB, cssPrecision) + ',' +
roundTo(-matrix.mValueC, cssPrecision) + ',' +
roundTo(matrix.mValueD, cssPrecision) + ',' +
roundTo(matrix.mValueTX, cssPrecision) + ',' +
roundTo(matrix.mValueTY, cssPrecision) + ');';
// TODO: handle character scaling.
// One option: add separate CSS transform to paragraphs inside a TextFrame
var charStyle = textFrame.textRange.characterAttributes;
var scaleX = charStyle.horizontalScale;
var scaleY = charStyle.verticalScale;
if (scaleX != 100 || scaleY != 100) {
warn('Vertical or horizontal text scaling will be lost. Affected text: ' + truncateString(textFrame.contents, 35));
}
return 'transform: ' + transform + 'transform-origin: ' + transformOrigin +
'-webkit-transform: ' + transform + '-webkit-transform-origin: ' + transformOrigin +
'-ms-transform: ' + transform + '-ms-transform-origin: ' + transformOrigin;
}
// Create class='' and style='' CSS for positioning the label container div
// (This container wraps one or more tags)
function getTextFrameCss(thisFrame, abBox, pgData, settings) {
var styles = '';
var classes = '';
// Using AI style of first paragraph in TextFrame to get information about
// tracking, justification and top padding
// TODO: consider positioning paragraphs separately, to handle pgs with different
// justification in the same text block
var firstPgStyle = pgData[0].aiStyle;
var lastPgStyle = pgData[pgData.length - 1].aiStyle;
var isRotated = firstPgStyle.rotated;
var aiBounds = isRotated ? getUntransformedTextBounds(thisFrame) : thisFrame.geometricBounds;
var htmlBox = convertAiBounds(shiftBounds(aiBounds, -abBox.left, abBox.top));
var thisFrameAttributes = parseDataAttributes(thisFrame.note);
// estimated space between top of HTML container and character glyphs
// (related to differences in AI and CSS vertical positioning of text blocks)
var marginTopPx = (firstPgStyle.leading - firstPgStyle.size) / 2 + firstPgStyle.spaceBefore;
// estimated space between bottom of HTML container and character glyphs
var marginBottomPx = (lastPgStyle.leading - lastPgStyle.size) / 2 + lastPgStyle.spaceAfter;
// var trackingPx = firstPgStyle.size * firstPgStyle.tracking / 1000;
var htmlL = htmlBox.left;
var htmlT = Math.round(htmlBox.top - marginTopPx);
var htmlW = htmlBox.width;
var htmlH = htmlBox.height + marginTopPx + marginBottomPx;
var alignment, v_align, vertAnchorPct;
if (firstPgStyle.justification == 'Justification.LEFT') {
alignment = 'left';
} else if (firstPgStyle.justification == 'Justification.RIGHT') {
alignment = 'right';
} else if (firstPgStyle.justification == 'Justification.CENTER') {
alignment = 'center';
}
if (thisFrame.kind == TextType.AREATEXT) {
v_align = 'top'; // area text aligned to top by default
// EXPERIMENTAL feature
// Put a box around the text, if the text frame's textPath is styled
styles += convertAreaTextPath(thisFrame);
} else { // point text
// point text aligned to midline (sensible default for chart y-axes, map labels, etc.)
v_align = 'middle';
htmlW += 22; // add a bit of extra width to try to prevent overflow
}
if (thisFrameAttributes.valign && !isRotated) {
// override default vertical alignment, unless text is rotated (TODO: support other )
v_align = thisFrameAttributes.valign;
if (v_align == 'center') {
v_align = 'middle';
}
}
if (isRotated) {
vertAnchorPct = (marginTopPx + htmlBox.height * 0.5 + 1) / (htmlH) * 100; // TODO: de-kludge
styles += getTransformationCss(thisFrame, vertAnchorPct);
// Only center alignment currently works well with rotated text
// TODO: simplify alignment of rotated text (some logic is in convertAiTextStyle())
v_align = 'middle';
alignment = 'center';
// text-align of point text set to 'center' in convertAiTextStyle()
}
if (v_align == 'bottom') {
var bottomPx = abBox.height - (htmlBox.top + htmlBox.height + marginBottomPx);
styles += 'bottom:' + formatCssPct(bottomPx, abBox.height) + ';';
} else if (v_align == 'middle') {
// https://css-tricks.com/centering-in-the-unknown/
// TODO: consider: http://zerosixthree.se/vertical-align-anything-with-just-3-lines-of-css/
styles += 'top:' + formatCssPct(htmlT + marginTopPx + htmlBox.height / 2, abBox.height) + ';';
styles += 'margin-top:-' + roundTo(marginTopPx + htmlBox.height / 2, 1) + 'px;';
} else {
styles += 'top:' + formatCssPct(htmlT, abBox.height) + ';';
}
if (alignment == 'right') {
styles += 'right:' + formatCssPct(abBox.width - (htmlL + htmlBox.width), abBox.width) + ';';
} else if (alignment == 'center') {
styles += 'left:' + formatCssPct(htmlL + htmlBox.width / 2, abBox.width) + ';';
// setting a negative left margin for horizontal placement of centered text
// using percent for area text (because area text width uses percent) and pixels for point text
if (thisFrame.kind == TextType.POINTTEXT) {
styles += 'margin-left:-' + roundTo(htmlW / 2, 1) + 'px;';
} else {
styles += 'margin-left:' + formatCssPct(-htmlW / 2, abBox.width )+ ';';
}
} else {
styles += 'left:' + formatCssPct(htmlL, abBox.width) + ';';
}
classes = nameSpace + getLayerName(thisFrame.layer) + ' ' + nameSpace + 'aiAbs';
if (thisFrame.kind == TextType.POINTTEXT) {
classes += ' ' + nameSpace + 'aiPointText';
// using pixel width with point text, because pct width causes alignment problems -- see issue #63
// adding extra pixels in case HTML width is slightly less than AI width (affects alignment of right-aligned text)
styles += 'width:' + roundTo(htmlW, cssPrecision) + 'px;';
} else if (settings.text_responsiveness == 'fixed') {
styles += 'width:' + roundTo(htmlW, cssPrecision) + 'px;';
} else {
// area text uses pct width, so width of text boxes will scale
// TODO: consider only using pct width with wider text boxes that contain paragraphs of text
styles += 'width:' + formatCssPct(htmlW, abBox.width) + ';';
}
return 'class="' + classes + '" style="' + styles + '"';
}
function convertAreaTextPath(frame) {
var style = '';
var path = frame.textPath;
var obj;
if (path.stroked || path.filled) {
style += 'padding: 6px 6px 6px 7px;';
if (path.filled) {
obj = convertAiColor(path.fillColor, path.opacity);
style += 'background-color: ' + obj.color + ';';
}
if (path.stroked) {
obj = convertAiColor(path.strokeColor, path.opacity);
style += 'border: 1px solid ' + obj.color + ';';
}
}
return style;
}
// =================================
// ai2html symbol functions
// =================================
// Return inline CSS for styling a single symbol
// TODO: create classes to capture style properties that are used repeatedly
function getBasicSymbolCss(geom, style, abBox, opts) {
var center = geom.center;
var styles = [];
// Round fixed-size symbols to integer size, to prevent pixel-snapping from
// changing squares and circles to rectangles and ovals.
var precision = opts.scaled ? 1 : 0;
var width, height;
var border;
if (geom.type == 'line') {
precision = 2;
width = geom.width;
height = geom.height;
if (width > height) {
// kludge to minimize gaps between segments (found using trial and error)
width += style.strokeWidth * 0.5;
center[0] += style.strokeWidth * 0.333;
}
} else if (geom.type == 'rectangle') {
width = geom.width;
height = geom.height;
} else if (geom.type == 'circle') {
width = geom.radius * 2;
height = width;
// styles.push('border-radius: ' + roundTo(geom.radius, 1) + 'px');
styles.push('border-radius: 50%');
}
width = roundTo(width, precision);
height = roundTo(height, precision);
if (opts.scaled) {
styles.push('width: ' + formatCssPct(width, abBox.width));
styles.push('height: ' + formatCssPct(height, abBox.height));
styles.push('margin-left: ' + formatCssPct(-width / 2, abBox.width));
// vertical margin pct is calculated as pct of width
styles.push('margin-top: ' + formatCssPct(-height / 2, abBox.width));
} else {
styles.push('width: ' + width + 'px');
styles.push('height: ' + height + 'px');
styles.push('margin-top: ' + (-height / 2) + 'px');
styles.push('margin-left: ' + (-width / 2) + 'px');
}
if (style.stroke) {
if (geom.type == 'line' && width > height) {
border = 'border-top';
} else if (geom.type == 'line') {
border = 'border-right';
} else {
border = 'border';
}
styles.push(border + ': ' + style.strokeWidth + 'px solid ' + style.stroke);
}
if (style.fill) {
styles.push('background-color: ' + style.fill);
}
if (style.opacity < 1 && style.opacity) {
styles.push('opacity: ' + style.opacity);
}
if (style.multiply) {
styles.push('mix-blend-mode: multiply');
}
styles.push('left: ' + formatCssPct(center[0], abBox.width));
styles.push('top: ' + formatCssPct(center[1], abBox.height));
// TODO: use class for colors and other properties
return 'style="' + styles.join('; ') + ';"';
}
function getSymbolClass() {
return nameSpace + 'aiSymbol';
}
function exportSymbolAsHtml(item, geometries, abBox, opts) {
var html = '';
var style = getBasicSymbolStyle(item);
var properties = item.name ? 'data-name="' + makeKeyword(item.name) + '" ' : '';
var geom, x, y;
for (var i=0; i';
}
return html;
}
function testEmptyArtboard(ab) {
return !testLayerArtboardIntersection(null, ab);
}
function testLayerArtboardIntersection(lyr, ab) {
if (lyr) {
return layerIsVisible(lyr);
} else {
return some(doc.layers, layerIsVisible);
}
function layerIsVisible(lyr) {
if (objectIsHidden(lyr)) return false;
return some(lyr.layers, layerIsVisible) ||
some(lyr.pageItems, itemIsVisible) ||
some(lyr.groupItems, groupIsVisible);
}
function itemIsVisible(item) {
if (item.hidden || item.guides || item.typename == "GroupItem") return false;
return testBoundsIntersection(item.visibleBounds, ab.artboardRect);
}
function groupIsVisible(group) {
if (group.hidden) return;
return some(group.pageItems, itemIsVisible) ||
some(group.groupItems, groupIsVisible);
}
}
// Convert paths representing simple shapes to HTML and hide them
function exportSymbols(lyr, ab, masks, opts) {
var items = [];
var abBox = convertAiBounds(ab.artboardRect);
var html = '';
forLayer(lyr);
function forLayer(lyr) {
// if (lyr.hidden) return; // bug -- layers use visible property, not hidden
if (objectIsHidden(lyr)) return;
forEach(lyr.pageItems, forPageItem);
forEach(lyr.layers, forLayer);
forEach(lyr.groupItems, forGroup);
}
function forGroup(group) {
if (group.hidden) return;
forEach(group.pageItems, forPageItem);
forEach(group.groupItems, forGroup);
}
function forPageItem(item) {
var singleGeom, geometries;
if (item.hidden || item.guides || !testBoundsIntersection(item.visibleBounds, ab.artboardRect)) return;
// try to convert to circle or rectangle
// note: filled shapes aren't necessarily closed
if (item.typename != 'PathItem') return;
singleGeom = getRectangleData(item.pathPoints) || getCircleData(item.pathPoints);
if (singleGeom) {
geometries = [singleGeom];
} else if (opts.scaled && item.stroked && !item.closed) {
// try to convert to line segment(s)
geometries = getLineGeometry(item.pathPoints);
}
if (!geometries) return; // item is not convertible to an HTML symbol
html += exportSymbolAsHtml(item, geometries, abBox, opts);
items.push(item);
item.hidden = true;
}
if (html) {
html = '\t\t' + html + '\r\t\t
\r';
}
return {
html: html,
items: items
};
}
function getBasicSymbolStyle(item) {
// TODO: handle opacity
var style = {};
var stroke, fill;
style.opacity = roundTo(getComputedOpacity(item) / 100, 2);
if (getBlendMode(item) == BlendModes.MULTIPLY) {
style.multiply = true;
}
if (item.filled) {
fill = convertAiColor(item.fillColor);
style.fill = fill.color;
}
if (item.stroked) {
stroke = convertAiColor(item.strokeColor);
style.stroke = stroke.color;
// Chrome doesn't consistently render borders that are less than 1px, which
// can cause lines to disappear or flicker as the window is resized.
style.strokeWidth = item.strokeWidth < 1 ? 1 : Math.round(item.strokeWidth);
}
return style;
}
function getPathBBox(points) {
var bbox = [Infinity, Infinity, -Infinity, -Infinity];
var p;
for (var i=0, n=points.length; i bbox[2]) bbox[2] = p[0];
if (p[1] < bbox[1]) bbox[1] = p[1];
if (p[1] > bbox[3]) bbox[3] = p[1];
}
return bbox;
}
function getBBoxCenter(bbox) {
return [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2];
}
// Return array of line records if path is composed only of vertical and/or
// horizontal line segments, else return null;
function getLineGeometry(points) {
var bbox, w, h, p;
var lines = [];
for (var i=0, n=points.length; i 1 && h > 1) return null; // diagonal line = fail
lines.push({
type: 'line',
center: getBBoxCenter(bbox),
width: w,
height: h
});
}
return lines.length > 0 ? lines : null;
}
function pathPointIsCorner(p) {
var xy = p.anchor;
// Vertices of polylines (often) use PointType.SMOOTH. Need to check control points
// to determine if the line is curved or not at p
// if (p.pointType != PointType.CORNER) return false;
if (xy[0] != p.leftDirection[0] || xy[0] != p.rightDirection[0] ||
xy[1] != p.leftDirection[1] || xy[1] != p.rightDirection[1]) return false;
return true;
}
// If path described by points array looks like a rectangle, return data for rendering
// as a rectangle; else return null
// points: an array of PathPoint objects
function getRectangleData(points) {
var bbox, p, xy;
// Some rectangles are 4-point closed paths, some are 5-point open paths
if (points.length < 4 || points.length > 5) return null;
bbox = getPathBBox(points);
for (var i=0; i<4; i++) {
p = points[i];
xy = p.anchor;
if (!pathPointIsCorner(p)) return null;
// point must be a bbox corner
if (xy[0] != bbox[0] && xy[0] != bbox[2] && xy[1] != bbox[1] && xy[1] != bbox[3]) {
return null;
}
}
return {
type: 'rectangle',
center: getBBoxCenter(bbox),
width: bbox[2] - bbox[0],
height: bbox[3] - bbox[1]
};
}
// If path described by points array looks like a circle, return data for rendering
// as a circle; else return null
// Assumes that circles have four anchor points at the top, right, bottom and left
// positions around the circle.
// points: an array of PathPoint objects
function getCircleData(points) {
var bbox, p, xy, edges;
if (points.length != 4) return null;
bbox = getPathBBox(points);
for (var i=0; i<4; i++) {
p = points[i];
xy = p.anchor;
// heuristic for identifying circles:
// * each vertex is "smooth"
// * either x or y coord of each vertex is on the bbox
if (p.pointType != PointType.SMOOTH) return null;
edges = 0;
if (xy[0] == bbox[0] || xy[0] == bbox[2]) edges++;
if (xy[1] == bbox[1] || xy[1] == bbox[3]) edges++;
if (edges != 1) return null;
}
return {
type: 'circle',
center: getBBoxCenter(bbox),
// radius is the average of vertical and horizontal half-axes
// ellipses are converted to circles
radius: (bbox[2] - bbox[0] + bbox[3] - bbox[1]) / 4
};
}
// =================================
// ai2html image functions
// =================================
function getArtboardImageName(ab, settings) {
return getArtboardUniqueName(ab, settings);
}
function getLayerImageName(lyr, ab, settings) {
return getArtboardImageName(ab, settings) + '-' + getLayerName(lyr);
}
function getImageId(imgName) {
return nameSpace + imgName + '-img';
}
function uniqAssetName(name, names) {
var uniqName = name;
var num = 2;
while (contains(names, uniqName)) {
uniqName = name + '-' + num;
num++;
}
return uniqName;
}
function getPromoImageFormat(ab, settings) {
var fmt = settings.image_format[0];
if (fmt == 'svg' || !fmt) {
fmt = 'png';
} else {
fmt = resolveArtboardImageFormat(fmt, ab);
}
return fmt;
}
// setting: value from ai2html settings (e.g. 'auto' 'png')
function resolveArtboardImageFormat(setting, ab) {
var fmt;
if (setting == 'auto') {
fmt = artboardContainsVisibleRasterImage(ab) ? 'jpg' : 'png';
} else {
fmt = setting;
}
return fmt;
}
function objectHasLayer(obj) {
var hasLayer = false;
try {
hasLayer = !!obj.layer;
} catch(e) {
// trying to access the layer property of a placed item that is used as an opacity mask
// throws an error (as of Illustrator 2018)
}
return hasLayer;
}
function artboardContainsVisibleRasterImage(ab) {
function test(item) {
// Calling objectHasLayer() prevents a crash caused by opacity masks created from linked rasters.
return objectHasLayer(item) && objectOverlapsArtboard(item, ab) && !objectIsHidden(item);
}
// TODO: verify that placed items are rasters
return contains(doc.placedItems, test) || contains(doc.rasterItems, test);
}
function convertSpecialLayers(activeArtboard, settings) {
var data = {
layers: [],
html_before: '',
html_after: '',
video: ''
};
forEach(findTaggedLayers('video'), function(lyr) {
if (objectIsHidden(lyr)) return;
var str = getSpecialLayerText(lyr, activeArtboard);
if (!str) return;
var html = makeVideoHtml(str, settings);
if (!html) {
warn('Invalid video URL: ' + str);
} else {
data.video = html;
}
data.layers.push(lyr);
});
forEach(findTaggedLayers('html-before'), function(lyr) {
if (objectIsHidden(lyr)) return;
var str = getSpecialLayerText(lyr, activeArtboard);
if (!str) return;
data.layers.push(lyr);
data.html_before = str;
});
forEach(findTaggedLayers('html-after'), function(lyr) {
if (objectIsHidden(lyr)) return;
var str = getSpecialLayerText(lyr, activeArtboard);
if (!str) return;
data.layers.push(lyr);
data.html_after = str;
});
return data.layers.length === 0 ? null : data;
}
function makeVideoHtml(url, settings) {
url = trim(url);
if (!/^https:/.test(url) || !/\.mp4$/.test(url)) {
return '';
}
var srcName = isTrue(settings.use_lazy_loader) ? 'data-src' : 'src';
return '';
}
function getSpecialLayerText(lyr, ab) {
var text = '';
forEach(lyr.textFrames, eachFrame);
function eachFrame(frame) {
if (testBoundsIntersection(frame.visibleBounds, ab.artboardRect)) {
text = frame.contents;
}
}
return text;
}
// Generate images and return HTML embed code
function convertArtItems(activeArtboard, textFrames, masks, settings) {
var imgName = getArtboardImageName(activeArtboard, settings);
var hideTextFrames = !isTrue(settings.testing_mode) && settings.render_text_as != 'image';
var textFrameCount = textFrames.length;
var html = '';
var uniqNames = [];
var hiddenItems = [];
var hiddenLayers = [];
var i;
checkForOutputFolder(getImageFolder(settings), 'image_output_path');
if (hideTextFrames) {
for (i=0; i png; other format names are same as extension
return name + '.' + fmt.substring(0, 3);
}
function getLayerOpacityCSS(layer) {
var o = getComputedOpacity(layer);
return o < 100 ? 'opacity:' + roundTo(o / 100, 2) + ';' : '';
}
// Capture and save an image to the filesystem and return html embed code
//
function exportImage(imgName, format, ab, masks, layer, settings) {
var imgFile = getImageFileName(imgName, format);
var outputPath = pathJoin(getImageFolder(settings), imgFile);
var imgId = getImageId(imgName);
// imgClass: // remove artboard size (careful not to remove deduplication annotations)
var imgClass = imgId.replace(/-[1-9][0-9]+-/, '-');
// all images are now absolutely positioned (before, artboard images were
// position:static to set the artboard height)
var inlineSvg = isTrue(settings.inline_svg) || (layer && parseObjectName(layer.name).inline);
var svgInlineStyle, svgLayersArg;
var created, html;
imgClass += ' ' + nameSpace + 'aiImg';
if (format == 'svg') {
if (layer) {
svgInlineStyle = getLayerOpacityCSS(layer);
svgLayersArg = [layer];
}
created = exportSVG(outputPath, ab, masks, svgLayersArg, settings);
if (!created) {
return ''; // no image was created
}
rewriteSVGFile(outputPath, imgId);
if (inlineSvg) {
html = generateInlineSvg(outputPath, imgClass, svgInlineStyle, settings);
if (layer) {
message('Generated inline SVG for layer [' + getLayerName(layer) + ']');
}
} else {
// generate link to external SVG file
html = generateImageHtml(imgFile, imgId, imgClass, svgInlineStyle, ab, settings);
if (layer) {
message('Exported an SVG layer as ' + outputPath.replace(/.*\//, ''));
}
}
} else {
// export raster image & generate link
exportRasterImage(outputPath, ab, format, settings);
html = generateImageHtml(imgFile, imgId, imgClass, null, ab, settings);
}
return html;
}
function generateInlineSvg(imgPath, imgClass, imgStyle, settings) {
var svg = readFile(imgPath) || '';
var attr = ' class="' + imgClass + '"';
if (imgStyle) {
attr += ' style="' + imgStyle + '"';
}
svg = svg.replace(/<\?xml.*?\?>/, '');
svg = svg.replace('', style + '\n');
}
// ===================================
// ai2html output generation functions
// ===================================
function generateArtboardDiv(ab, group, settings) {
var id = nameSpace + getArtboardUniqueName(ab, settings);
var classname = nameSpace + 'artboard';
var widthRange = getArtboardWidthRange(ab, group, settings);
var visibleRange = getArtboardVisibilityRange(ab, group, settings);
var abBox = convertAiBounds(ab.artboardRect);
var aspectRatio = abBox.width / abBox.height;
var inlineStyle = '';
var inlineSpacerStyle = '';
var html = '';
// Set size of graphic using inline CSS
if (widthRange[0] == widthRange[1]) {
// fixed width
// inlineSpacerStyle += "width:" + abBox.width + "px; height:" + abBox.height + "px;";
inlineStyle += 'width:' + abBox.width + 'px; height:' + abBox.height + 'px;';
} else {
// Set height of dynamic artboards using vertical padding as a %, to preserve aspect ratio.
inlineSpacerStyle = 'padding: 0 0 ' + formatCssPct(abBox.height, abBox.width) + ' 0;';
if (widthRange[0] > 0) {
inlineStyle += 'min-width: ' + widthRange[0] + 'px;';
}
if (widthRange[1] < Infinity) {
inlineStyle += 'max-width: ' + widthRange[1] + 'px;';
inlineStyle += 'max-height: ' + Math.round(widthRange[1] / aspectRatio) + 'px';
}
}
html += '\t\r';
// add spacer div
html += '\t\t
\n';
return html;
}
function generateArtboardCss(ab, group, cssRules, settings) {
var abId = '#' + nameSpace + getArtboardUniqueName(ab, settings),
css = formatCssRule(abId, {
position: 'relative',
overflow: 'hidden'
});
if (isTrue(settings.include_resizer_css)) {
css += generateContainerQueryCss(ab, abId, group, settings);
}
// classes for paragraph and character styles
forEach(cssRules, function(cssBlock) {
css += abId + ' ' + cssBlock;
});
return css;
}
function generateContainerQueryCss(ab, abId, group, settings) {
var css = '';
var visibleRange = getArtboardVisibilityRange(ab, group, settings);
var isSmallest = visibleRange[0] === 0;
var isLargest = visibleRange[1] === Infinity;
var query;
if (isSmallest && isLargest) {
// single artboard: no query needed
return '';
}
// default visibility: smallest ab visible, others hidden
// (fallback in case browser doesn't support container queries)
if (!isSmallest) {
css += formatCssRule(abId, {display: 'none'});
}
// compose container query
if (isSmallest) {
query = '(width >= ' + (visibleRange[1] + 1) + 'px)';
} else {
query = '(width >= ' + visibleRange[0] + 'px)';
if (!isLargest) {
query += ' and (width < ' + (visibleRange[1] + 1) + 'px)';
}
}
css += '@container ' + getGroupContainerId(group.groupName) + ' ' + query + ' {\r';
css += formatCssRule(abId, { display: isSmallest ? 'none' : 'block' });
css += '}\r';
return css;
}
// Get CSS styles that are common to all generated content
function generatePageCss(containerId, group, settings) {
var css = '';
var blockStart = '#' + containerId;
if (isTrue(settings.include_resizer_css) && group.artboards.length > 1) {
css += formatCssRule(blockStart, {
'container-type': 'inline-size',
'container-name': containerId
});
}
if (settings.max_width) {
css += formatCssRule(blockStart,
{'max-width': settings.max_width + 'px'});
}
if (isTrue(settings.center_html_output)) {
css += formatCssRule(
blockStart + ',\r' + blockStart + ' .' + nameSpace + 'artboard',
{margin: '0 auto'});
}
if (settings.alt_text) {
css += formatCssRule(blockStart + ' .' + nameSpace + 'aiAltText', {
position: 'absolute',
left: '-10000px',
width: '1px',
height: '1px',
overflow: 'hidden',
'white-space': 'nowrap'
});
}
if (settings.clickable_link !== '') {
css += formatCssRule(blockStart + ' .' + nameSpace + 'ai2htmlLink',
{display: 'block'});
}
// default
styles
css += formatCssRule(blockStart + ' p', {margin: '0'});
if (isTrue(settings.testing_mode)) {
css += formatCssRule(blockStart + ' p',
{color: 'rgba(209, 0, 0, 0.5) !important'});
}
css += formatCssRule(blockStart + ' .' + nameSpace + 'aiAbs', {position: 'absolute'});
css += formatCssRule(blockStart + ' .' + nameSpace + 'aiImg', {
position: 'absolute',
top: '0',
display: 'block',
width: '100% !important'
});
css += formatCssRule(blockStart + ' .' + getSymbolClass(),
{position: 'absolute', 'box-sizing': 'border-box'});
css += formatCssRule(blockStart + ' .' + nameSpace + 'aiPointText p',
{'white-space': 'nowrap'});
return css;
}
function getCommonOutputSettings(settings) {
var range = getWidthRangeForConfig(settings);
return {
ai2html_version: scriptVersion,
project_type: 'ai2html',
min_width: range[0],
max_width: range[1],
tags: 'ai2html',
type: 'embeddedinteractive'
}
}
function generateJsonSettingsFileContent(settings) {
var o = getCommonOutputSettings(settings);
forEach(settings.config_file, function(key) {
var val = String(settings[key]);
if (isTrue(val)) val = true;
else if (isFalse(val)) val = false;
o[key] = val;
});
return JSON.stringify(o, null, 2);
}
// Create a settings file (optimized for the NYT Scoop CMS)
function generateYamlFileContent(settings) {
var o = getCommonOutputSettings(settings);
var lines = [];
lines.push('ai2html_version: ' + scriptVersion);
if (settings.project_type) {
lines.push('project_type: ' + settings.project_type);
}
lines.push('type: ' + o.type);
lines.push('tags: ' + o.tags);
lines.push('min_width: ' + o.min_width);
lines.push('max_width: ' + o.max_width);
if (isTrue(settings.dark_mode_compatible)) {
// kludge to output YAML array value for one setting
lines.push('display_overrides:\n - DARK_MODE_COMPATIBLE');
}
forEach(settings.config_file, function(key) {
var value = trim(String(settings[key]));
var useQuotes = value === '' || /\s/.test(value);
if (key == 'show_in_compatible_apps') {
// special case: this setting takes quoted 'yes' or 'no'
useQuotes = true; // assuming value is 'yes' or 'no';
value = isTrue(value) ? 'yes' : 'no';
}
if (useQuotes) {
value = JSON.stringify(value); // wrap in quotes and escape internal quotes
} else if (isTrue(value) || isFalse(value)) {
// use standard values for boolean settings
value = isTrue(value) ? 'true' : 'false';
}
lines.push(key + ': ' + value);
});
return lines.join('\n');
}
function getResizerScript(containerId) {
// The resizer function is embedded in the HTML page -- external variables must
// be passed in.
//
// TODO: Consider making artboard images position:absolute and setting
// height as a padding % (calculated from the aspect ratio of the graphic).
// This will correctly set the initial height of the graphic before
// an image is loaded.
//
var resizer = function (containerId, opts) {
var nameSpace = opts.namespace || '';
var containers = findContainers(containerId);
containers.forEach(resize);
function resize(container) {
var onResize = throttle(update, 200);
var waiting = !!window.IntersectionObserver;
var observer;
update();
document.addEventListener('DOMContentLoaded', update);
window.addEventListener('resize', onResize);
// NYT Scoop-specific code
if (opts.setup) {
opts.setup(container).on('cleanup', cleanup);
}
function cleanup() {
document.removeEventListener('DOMContentLoaded', update);
window.removeEventListener('resize', onResize);
if (observer) observer.disconnect();
}
function update() {
var artboards = selectChildren('.' + nameSpace + 'artboard[data-min-width]', container),
width = Math.round(container.getBoundingClientRect().width);
// Set artboard visibility based on container width
artboards.forEach(function(el) {
var minwidth = el.getAttribute('data-min-width'),
maxwidth = el.getAttribute('data-max-width');
if (+minwidth <= width && (+maxwidth >= width || maxwidth === null)) {
if (!waiting) {
selectChildren('.' + nameSpace + 'aiImg', el).forEach(updateImgSrc);
selectChildren('video', el).forEach(updateVideoSrc);
}
el.style.display = 'block';
} else {
el.style.display = 'none';
}
});
// Initialize lazy loading on first call
if (waiting && !observer) {
if (elementInView(container)) {
waiting = false;
update();
} else {
observer = new IntersectionObserver(onIntersectionChange, {rootMargin: "800px"});
observer.observe(container);
}
}
}
function onIntersectionChange(entries) {
// There may be multiple entries relating to the same container
// (captured at different times)
var isIntersecting = entries.reduce(function(memo, entry) {
return memo || entry.isIntersecting;
}, false);
if (isIntersecting) {
waiting = false;
// update: don't remove -- we need the observer to trigger an update
// when a hidden map becomes visible after user interaction
// (e.g. when an accordion menu or tab opens)
// observer.disconnect();
// observer = null;
update();
}
}
}
function findContainers(id) {
// support duplicate ids on the page
return selectChildren('.ai2html-responsive', document).filter(function(el) {
if (el.getAttribute('id') != id) return false;
if (el.classList.contains('ai2html-resizer')) return false;
el.classList.add('ai2html-resizer');
return true;
});
}
// Replace blank placeholder image with actual image
function updateImgSrc(img) {
var src = img.getAttribute('data-src');
if (src && img.getAttribute('src') != src) {
img.setAttribute('src', src);
}
}
function updateVideoSrc(el) {
var src = el.getAttribute('data-src');
if (src && !el.hasAttribute('src')) {
el.setAttribute('src', src);
}
}
function elementInView(el) {
var bounds = el.getBoundingClientRect();
return bounds.top < window.innerHeight && bounds.bottom > 0;
}
function selectChildren(selector, parent) {
return parent ? Array.prototype.slice.call(parent.querySelectorAll(selector)) : [];
}
// based on underscore.js
function throttle(func, wait) {
var timeout = null, previous = 0;
function run() {
previous = Date.now();
timeout = null;
func();
}
return function() {
var remaining = wait - (Date.now() - previous);
if (remaining <= 0 || remaining > wait) {
clearTimeout(timeout);
run();
} else if (!timeout) {
timeout = setTimeout(run, remaining);
}
};
}
};
var optStr = '{namespace: "' + nameSpace + '", setup: window.setupInteractive || window.getComponent}';
// convert resizer function to JS source code
var resizerJs = '(' +
trim(resizer.toString().replace(/ {2}/g, '\t')) + // indent with tabs
')("' + containerId + '", ' + optStr + ');';
return '\r';
}
// Write an HTML page to a file for NYT Preview
function outputLocalPreviewPage(textForFile, localPreviewDestination, settings) {
var localPreviewTemplateText = readTextFile(docPath + settings.local_preview_template);
settings.ai2htmlPartial = textForFile; // TODO: don't modify global settings this way
var localPreviewHtml = applyTemplate(localPreviewTemplateText, settings);
saveTextFile(localPreviewDestination, localPreviewHtml);
}
function addTextBlockContent(output, content) {
if (content.css) {
output.css += '\r/* Custom CSS */\r' + content.css.join('\r') + '\r';
}
if (content['html-before']) {
output.html += '\r' + content['html-before'].join('\r') + '\r' + output.html + '\r';
}
if (content['html-after']) {
output.html += '\r\r' + content['html-after'].join('\r') + '\r';
}
// deprecated
if (content.html) {
output.html += '\r\r' + content.html.join('\r') + '\r';
}
// TODO: assumed JS contained in