diff --git a/package.json b/package.json index 1f7e9d7..d6a464e 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,6 @@ "homepage": "https://html2canvas.hertzen.com", "license": "MIT", "dependencies": { - "punycode": "2.1.0", - "liststyletype-formatter": "latest" + "punycode": "2.1.0" } } diff --git a/src/ListItem.js b/src/ListItem.js index 78acf1c..65ee9f8 100644 --- a/src/ListItem.js +++ b/src/ListItem.js @@ -1,135 +1,101 @@ /* @flow */ 'use strict'; -import type {BackgroundSource} from './parsing/background'; import type ResourceLoader from './ResourceLoader'; +import type {ListStyleType} from './parsing/listStyle'; -import {parseBackgroundImage} from './parsing/background'; -import {copyCSSStyles, getParentOfType} from './Util'; +import {copyCSSStyles, contains} from './Util'; import NodeContainer from './NodeContainer'; import TextContainer from './TextContainer'; -import ListStyleTypeFormatter from 'liststyletype-formatter'; +import {LIST_STYLE_POSITION, LIST_STYLE_TYPE} from './parsing/listStyle'; +import {fromCodePoint} from './Unicode'; // Margin between the enumeration and the list item content const MARGIN_RIGHT = 7; -export const LIST_STYLE_POSITION = { - INSIDE: 0, - OUTSIDE: 1 -}; +const ancestorTypes = ['OL', 'UL', 'MENU']; -export type ListStylePosition = $Values; - -export type ListStyle = { - listStyleType: string, - listStyleImage: BackgroundSource, - listStylePosition: ListStylePosition -}; - -export const parseListStyle = (style: CSSStyleDeclaration): ListStyle => { - const listStyleImage = parseBackgroundImage(style.getPropertyValue('list-style-image')); - return { - listStyleType: style.getPropertyValue('list-style-type'), - listStyleImage: listStyleImage && listStyleImage[0], - listStylePosition: parseListStylePosition(style.getPropertyValue('list-style-position')) - }; -}; - -export const parseListStylePosition = (position: string): ListStylePosition => { - switch (position) { - case 'inside': - return LIST_STYLE_POSITION.INSIDE; - case 'outside': - return LIST_STYLE_POSITION.OUTSIDE; - } - return LIST_STYLE_POSITION.OUTSIDE; -}; - -const getListItemValue = (node: HTMLLIElement): number => { - if (node.value) { - return node.value; +export const getListOwner = (container: NodeContainer): ?NodeContainer => { + let parent = container.parent; + if (!parent) { + return null; } - const listContainer = getParentOfType(node, ['OL', 'UL']); - if (!listContainer || listContainer.tagName === 'UL') { - // The actual value isn't needed for unordered lists, just return an arbitrary value - return 1; - } - - // $FlowFixMe - let value = listContainer.start !== undefined ? listContainer.start - 1 : 0; - const listItems = listContainer.querySelectorAll('li'); - const lenListItems = listItems.length; - - for (let i = 0; i < lenListItems; i++) { - // $FlowFixMe - const listItem: HTMLLIElement = listItems[i]; - if (getParentOfType(listItem, ['OL']) === listContainer) { - value = listItem.hasAttribute('value') ? listItem.value : value + 1; + do { + let isAncestor = ancestorTypes.indexOf(parent.tagName) !== -1; + if (isAncestor) { + return parent; } - if (listItem === node) { - break; - } - } + parent = parent.parent; + } while (parent); - return value; + return container.parent; }; export const inlineListItemElement = ( - node: HTMLLIElement, + node: HTMLElement, container: NodeContainer, resourceLoader: ResourceLoader ): void => { - const style = node.ownerDocument.defaultView.getComputedStyle(node, null); - const listStyle = parseListStyle(style); + const listStyle = container.style.listStyle; - if (listStyle.listStyleType === 'none') { + if (!listStyle) { return; } + const style = node.ownerDocument.defaultView.getComputedStyle(node, null); const wrapper = node.ownerDocument.createElement('html2canvaswrapper'); copyCSSStyles(style, wrapper); - wrapper.style.position = 'fixed'; + wrapper.style.position = 'absolute'; wrapper.style.bottom = 'auto'; + wrapper.style.display = 'block'; + wrapper.style.letterSpacing = 'normal'; switch (listStyle.listStylePosition) { case LIST_STYLE_POSITION.OUTSIDE: wrapper.style.left = 'auto'; wrapper.style.right = `${node.ownerDocument.defaultView.innerWidth - - container.bounds.left + + container.bounds.left - + container.style.margin[1].getAbsoluteValue(container.bounds.width) + MARGIN_RIGHT}px`; wrapper.style.textAlign = 'right'; break; case LIST_STYLE_POSITION.INSIDE: - wrapper.style.left = `${container.bounds.left}px`; + wrapper.style.left = `${container.bounds.left - + container.style.margin[3].getAbsoluteValue(container.bounds.width)}px`; wrapper.style.right = 'auto'; wrapper.style.textAlign = 'left'; break; } let text; - if (listStyle.listStyleImage && listStyle.listStyleImage !== 'none') { - if (listStyle.listStyleImage.method === 'url') { + const MARGIN_TOP = container.style.margin[0].getAbsoluteValue(container.bounds.width); + const styleImage = listStyle.listStyleImage; + if (styleImage) { + if (styleImage.method === 'url') { const image = node.ownerDocument.createElement('img'); - image.src = listStyle.listStyleImage.args[0]; - wrapper.style.top = `${container.bounds.top}px`; + image.src = styleImage.args[0]; + wrapper.style.top = `${container.bounds.top - MARGIN_TOP}px`; wrapper.style.width = 'auto'; wrapper.style.height = 'auto'; wrapper.appendChild(image); } else { const size = parseFloat(container.style.font.fontSize) * 0.5; - wrapper.style.top = `${container.bounds.top + container.bounds.height - 1.5 * size}px`; + wrapper.style.top = `${container.bounds.top - + MARGIN_TOP + + container.bounds.height - + 1.5 * size}px`; wrapper.style.width = `${size}px`; wrapper.style.height = `${size}px`; wrapper.style.backgroundImage = style.listStyleImage; } - } else { + } else if (typeof container.listIndex === 'number') { text = node.ownerDocument.createTextNode( - ListStyleTypeFormatter.format(getListItemValue(node), style.listStyleType) + createCounterText(container.listIndex, listStyle.listStyleType) ); wrapper.appendChild(text); - wrapper.style.top = `${container.bounds.top}px`; + wrapper.style.top = `${container.bounds.top - MARGIN_TOP}px`; } // $FlowFixMe @@ -144,3 +110,565 @@ export const inlineListItemElement = ( container.childNodes.push(new NodeContainer(wrapper, container, resourceLoader, 0)); } }; + +const ROMAN_UPPER = { + integers: [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1], + values: ['M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I'] +}; + +const ARMENIAN = { + integers: [ + 9000, + 8000, + 7000, + 6000, + 5000, + 4000, + 3000, + 2000, + 1000, + 900, + 800, + 700, + 600, + 500, + 400, + 300, + 200, + 100, + 90, + 80, + 70, + 60, + 50, + 40, + 30, + 20, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1 + ], + values: [ + 'Ք', + 'Փ', + 'Ւ', + 'Ց', + 'Ր', + 'Տ', + 'Վ', + 'Ս', + 'Ռ', + 'Ջ', + 'Պ', + 'Չ', + 'Ո', + 'Շ', + 'Ն', + 'Յ', + 'Մ', + 'Ճ', + 'Ղ', + 'Ձ', + 'Հ', + 'Կ', + 'Ծ', + 'Խ', + 'Լ', + 'Ի', + 'Ժ', + 'Թ', + 'Ը', + 'Է', + 'Զ', + 'Ե', + 'Դ', + 'Գ', + 'Բ', + 'Ա' + ] +}; + +const HEBREW = { + integers: [ + 10000, + 9000, + 8000, + 7000, + 6000, + 5000, + 4000, + 3000, + 2000, + 1000, + 400, + 300, + 200, + 100, + 90, + 80, + 70, + 60, + 50, + 40, + 30, + 20, + 19, + 18, + 17, + 16, + 15, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1 + ], + values: [ + 'י׳', + 'ט׳', + 'ח׳', + 'ז׳', + 'ו׳', + 'ה׳', + 'ד׳', + 'ג׳', + 'ב׳', + 'א׳', + 'ת', + 'ש', + 'ר', + 'ק', + 'צ', + 'פ', + 'ע', + 'ס', + 'נ', + 'מ', + 'ל', + 'כ', + 'יט', + 'יח', + 'יז', + 'טז', + 'טו', + 'י', + 'ט', + 'ח', + 'ז', + 'ו', + 'ה', + 'ד', + 'ג', + 'ב', + 'א' + ] +}; + +const GEORGIAN = { + integers: [ + 10000, + 9000, + 8000, + 7000, + 6000, + 5000, + 4000, + 3000, + 2000, + 1000, + 900, + 800, + 700, + 600, + 500, + 400, + 300, + 200, + 100, + 90, + 80, + 70, + 60, + 50, + 40, + 30, + 20, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1 + ], + values: [ + 'ჵ', + 'ჰ', + 'ჯ', + 'ჴ', + 'ხ', + 'ჭ', + 'წ', + 'ძ', + 'ც', + 'ჩ', + 'შ', + 'ყ', + 'ღ', + 'ქ', + 'ფ', + 'ჳ', + 'ტ', + 'ს', + 'რ', + 'ჟ', + 'პ', + 'ო', + 'ჲ', + 'ნ', + 'მ', + 'ლ', + 'კ', + 'ი', + 'თ', + 'ჱ', + 'ზ', + 'ვ', + 'ე', + 'დ', + 'გ', + 'ბ', + 'ა' + ] +}; + +const createAdditiveCounter = ( + value: number, + min: number, + max: number, + symbols, + fallback: ListStyleType, + suffix: string = '. ' +) => { + if (value < min || value > max) { + return createCounterText(value, fallback); + } + + return ( + symbols.integers.reduce((string, integer, index) => { + while (value >= integer) { + value -= integer; + string += symbols.values[index]; + } + return string; + }, '') + suffix + ); +}; + +const createCounterStyleWithSymbolResolver = ( + value: number, + codePointRangeLength: number, + isNumeric: boolean, + resolver +): string => { + let string = ''; + + do { + if (!isNumeric) { + value--; + } + string = resolver(value) + string; + value /= codePointRangeLength; + } while (value * codePointRangeLength >= codePointRangeLength); + + return string; +}; + +const createCounterStyleFromRange = ( + value: number, + codePointRangeStart: number, + codePointRangeEnd: number, + isNumeric: boolean, + suffix: string = '. ' +): string => { + const codePointRangeLength = codePointRangeEnd - codePointRangeStart + 1; + + return ( + (value < 0 ? '-' : '') + + (createCounterStyleWithSymbolResolver( + Math.abs(value), + codePointRangeLength, + isNumeric, + codePoint => + fromCodePoint(Math.floor(codePoint % codePointRangeLength) + codePointRangeStart) + ) + + suffix) + ); +}; + +const createCounterStyleFromSymbols = ( + value: number, + symbols: string, + suffix: string = '. ' +): string => { + const codePointRangeLength = symbols.length; + return ( + createCounterStyleWithSymbolResolver( + Math.abs(value), + codePointRangeLength, + false, + codePoint => symbols[Math.floor(codePoint % codePointRangeLength)] + ) + suffix + ); +}; + +const CJK_ZEROS = 1 << 0; +const CJK_TEN_COEFFICIENTS = 1 << 1; +const CJK_TEN_HIGH_COEFFICIENTS = 1 << 2; +const CJK_HUNDRED_COEFFICIENTS = 1 << 3; + +const createCJKCounter = ( + value: number, + numbers: string, + multipliers: string, + negativeSign: string, + suffix: string, + flags: number +): string => { + if (value < -9999 || value > 9999) { + return createCounterText(value, LIST_STYLE_TYPE.CJK_DECIMAL); + } + let tmp = Math.abs(value); + let string = suffix; + + if (tmp === 0) { + return numbers[0] + string; + } + + for (let digit = 0; tmp > 0 && digit <= 4; digit++) { + let coefficient = tmp % 10; + + if (coefficient === 0 && contains(flags, CJK_ZEROS) && string !== '') { + string = numbers[coefficient] + string; + } else if ( + coefficient > 1 || + (coefficient === 1 && digit === 0) || + (coefficient === 1 && digit === 1 && contains(flags, CJK_TEN_COEFFICIENTS)) || + (coefficient === 1 && + digit === 1 && + contains(flags, CJK_TEN_HIGH_COEFFICIENTS) && + value > 100) || + (coefficient === 1 && digit > 1 && contains(flags, CJK_HUNDRED_COEFFICIENTS)) + ) { + string = numbers[coefficient] + (digit > 0 ? multipliers[digit - 1] : '') + string; + } else if (coefficient === 1 && digit > 0) { + string = multipliers[digit - 1] + string; + } + tmp = Math.floor(tmp / 10); + } + + return (value < 0 ? negativeSign : '') + string; +}; + +const CHINESE_INFORMAL_MULTIPLIERS = '十百千萬'; +const CHINESE_FORMAL_MULTIPLIERS = '拾佰仟萬'; +const JAPANESE_NEGATIVE = 'マイナス'; +const KOREAN_NEGATIVE = '마이너스 '; + +const createCounterText = (value: number, type: ListStyleType): string => { + switch (type) { + case LIST_STYLE_TYPE.DISC: + return '•'; + case LIST_STYLE_TYPE.CIRCLE: + return '◦'; + case LIST_STYLE_TYPE.SQUARE: + return '◾'; + case LIST_STYLE_TYPE.DECIMAL_LEADING_ZERO: + const string = createCounterStyleFromRange(value, 48, 57, true); + return string.length < 4 ? `0${string}` : string; + case LIST_STYLE_TYPE.CJK_DECIMAL: + return createCounterStyleFromSymbols(value, '〇一二三四五六七八九', '、'); + case LIST_STYLE_TYPE.LOWER_ROMAN: + return createAdditiveCounter( + value, + 1, + 3999, + ROMAN_UPPER, + LIST_STYLE_TYPE.DECIMAL + ).toLowerCase(); + case LIST_STYLE_TYPE.UPPER_ROMAN: + return createAdditiveCounter(value, 1, 3999, ROMAN_UPPER, LIST_STYLE_TYPE.DECIMAL); + case LIST_STYLE_TYPE.LOWER_GREEK: + return createCounterStyleFromRange(value, 945, 969, false); + case LIST_STYLE_TYPE.LOWER_ALPHA: + return createCounterStyleFromRange(value, 97, 122, false); + case LIST_STYLE_TYPE.UPPER_ALPHA: + return createCounterStyleFromRange(value, 65, 90, false); + case LIST_STYLE_TYPE.ARABIC_INDIC: + return createCounterStyleFromRange(value, 1632, 1641, true); + case LIST_STYLE_TYPE.ARMENIAN: + case LIST_STYLE_TYPE.UPPER_ARMENIAN: + return createAdditiveCounter(value, 1, 9999, ARMENIAN, LIST_STYLE_TYPE.DECIMAL); + case LIST_STYLE_TYPE.LOWER_ARMENIAN: + return createAdditiveCounter( + value, + 1, + 9999, + ARMENIAN, + LIST_STYLE_TYPE.DECIMAL + ).toLowerCase(); + case LIST_STYLE_TYPE.BENGALI: + return createCounterStyleFromRange(value, 2534, 2543, true); + case LIST_STYLE_TYPE.CAMBODIAN: + case LIST_STYLE_TYPE.KHMER: + return createCounterStyleFromRange(value, 6112, 6121, true); + case LIST_STYLE_TYPE.CJK_EARTHLY_BRANCH: + return createCounterStyleFromSymbols(value, '子丑寅卯辰巳午未申酉戌亥', '、'); + case LIST_STYLE_TYPE.CJK_HEAVENLY_STEM: + return createCounterStyleFromSymbols(value, '甲乙丙丁戊己庚辛壬癸', '、'); + case LIST_STYLE_TYPE.CJK_IDEOGRAPHIC: + case LIST_STYLE_TYPE.TRAD_CHINESE_INFORMAL: + return createCJKCounter( + value, + '零一二三四五六七八九', + CHINESE_INFORMAL_MULTIPLIERS, + '負', + '、', + CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS | CJK_HUNDRED_COEFFICIENTS + ); + case LIST_STYLE_TYPE.TRAD_CHINESE_FORMAL: + return createCJKCounter( + value, + '零壹貳參肆伍陸柒捌玖', + CHINESE_FORMAL_MULTIPLIERS, + '負', + '、', + CJK_ZEROS | + CJK_TEN_COEFFICIENTS | + CJK_TEN_HIGH_COEFFICIENTS | + CJK_HUNDRED_COEFFICIENTS + ); + case LIST_STYLE_TYPE.SIMP_CHINESE_INFORMAL: + return createCJKCounter( + value, + '零一二三四五六七八九', + CHINESE_INFORMAL_MULTIPLIERS, + '负', + '、', + CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS | CJK_HUNDRED_COEFFICIENTS + ); + case LIST_STYLE_TYPE.SIMP_CHINESE_FORMAL: + return createCJKCounter( + value, + '零壹贰叁肆伍陆柒捌玖', + CHINESE_FORMAL_MULTIPLIERS, + '负', + '、', + CJK_ZEROS | + CJK_TEN_COEFFICIENTS | + CJK_TEN_HIGH_COEFFICIENTS | + CJK_HUNDRED_COEFFICIENTS + ); + case LIST_STYLE_TYPE.JAPANESE_INFORMAL: + return createCJKCounter(value, '〇一二三四五六七八九', '十百千万', JAPANESE_NEGATIVE, '、', 0); + case LIST_STYLE_TYPE.JAPANESE_FORMAL: + return createCJKCounter( + value, + '零壱弐参四伍六七八九', + '拾百千万', + JAPANESE_NEGATIVE, + '、', + CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS + ); + case LIST_STYLE_TYPE.KOREAN_HANGUL_FORMAL: + return createCJKCounter( + value, + '영일이삼사오육칠팔구', + '십백천만', + KOREAN_NEGATIVE, + ', ', + CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS + ); + case LIST_STYLE_TYPE.KOREAN_HANJA_INFORMAL: + return createCJKCounter(value, '零一二三四五六七八九', '十百千萬', KOREAN_NEGATIVE, ', ', 0); + case LIST_STYLE_TYPE.KOREAN_HANJA_FORMAL: + return createCJKCounter( + value, + '零壹貳參四五六七八九', + '拾百千', + KOREAN_NEGATIVE, + ', ', + CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS + ); + case LIST_STYLE_TYPE.DEVANAGARI: + return createCounterStyleFromRange(value, 0x966, 0x96f, true); + case LIST_STYLE_TYPE.GEORGIAN: + return createAdditiveCounter(value, 1, 19999, GEORGIAN, LIST_STYLE_TYPE.DECIMAL); + case LIST_STYLE_TYPE.GUJARATI: + return createCounterStyleFromRange(value, 0xae6, 0xaef, true); + case LIST_STYLE_TYPE.GURMUKHI: + return createCounterStyleFromRange(value, 0xa66, 0xa6f, true); + case LIST_STYLE_TYPE.HEBREW: + return createAdditiveCounter(value, 1, 10999, HEBREW, LIST_STYLE_TYPE.DECIMAL); + case LIST_STYLE_TYPE.HIRAGANA: + return createCounterStyleFromSymbols( + value, + 'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわゐゑをん' + ); + case LIST_STYLE_TYPE.HIRAGANA_IROHA: + return createCounterStyleFromSymbols( + value, + 'いろはにほへとちりぬるをわかよたれそつねならむうゐのおくやまけふこえてあさきゆめみしゑひもせす' + ); + case LIST_STYLE_TYPE.KANNADA: + return createCounterStyleFromRange(value, 0xce6, 0xcef, true); + case LIST_STYLE_TYPE.KATAKANA: + return createCounterStyleFromSymbols( + value, + 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヰヱヲン', + '、' + ); + case LIST_STYLE_TYPE.KATAKANA_IROHA: + return createCounterStyleFromSymbols( + value, + 'イロハニホヘトチリヌルヲワカヨタレソツネナラムウヰノオクヤマケフコエテアサキユメミシヱヒモセス', + '、' + ); + case LIST_STYLE_TYPE.LAO: + return createCounterStyleFromRange(value, 0xed0, 0xed9, true); + case LIST_STYLE_TYPE.MONGOLIAN: + return createCounterStyleFromRange(value, 0x1810, 0x1819, true); + case LIST_STYLE_TYPE.MYANMAR: + return createCounterStyleFromRange(value, 0x1040, 0x1049, true); + case LIST_STYLE_TYPE.ORIYA: + return createCounterStyleFromRange(value, 0xb66, 0xb6f, true); + case LIST_STYLE_TYPE.PERSIAN: + return createCounterStyleFromRange(value, 0x6f0, 0x6f9, true); + case LIST_STYLE_TYPE.TAMIL: + return createCounterStyleFromRange(value, 0xbe6, 0xbef, true); + case LIST_STYLE_TYPE.TELUGU: + return createCounterStyleFromRange(value, 0xc66, 0xc6f, true); + case LIST_STYLE_TYPE.THAI: + return createCounterStyleFromRange(value, 0xe50, 0xe59, true); + case LIST_STYLE_TYPE.TIBETAN: + return createCounterStyleFromRange(value, 0xf20, 0xf29, true); + case LIST_STYLE_TYPE.DECIMAL: + default: + return createCounterStyleFromRange(value, 48, 57, true); + } +}; diff --git a/src/NodeContainer.js b/src/NodeContainer.js index 45218aa..8dfd0fa 100644 --- a/src/NodeContainer.js +++ b/src/NodeContainer.js @@ -7,6 +7,8 @@ import type {BorderRadius} from './parsing/borderRadius'; import type {DisplayBit} from './parsing/display'; import type {Float} from './parsing/float'; import type {Font} from './parsing/font'; +import type {ListStyle} from './parsing/listStyle'; +import type {Margin} from './parsing/margin'; import type {Overflow} from './parsing/overflow'; import type {Padding} from './parsing/padding'; import type {Position} from './parsing/position'; @@ -32,6 +34,8 @@ import {parseDisplay, DISPLAY} from './parsing/display'; import {parseCSSFloat, FLOAT} from './parsing/float'; import {parseFont} from './parsing/font'; import {parseLetterSpacing} from './parsing/letterSpacing'; +import {parseListStyle} from './parsing/listStyle'; +import {parseMargin} from './parsing/margin'; import {parseOverflow, OVERFLOW} from './parsing/overflow'; import {parsePadding} from './parsing/padding'; import {parsePosition, POSITION} from './parsing/position'; @@ -50,6 +54,7 @@ import { getInputBorderRadius, reformatInputBounds } from './Input'; +import {getListOwner} from './ListItem'; type StyleDeclaration = { background: Background, @@ -60,6 +65,8 @@ type StyleDeclaration = { float: Float, font: Font, letterSpacing: number, + listStyle: ListStyle | null, + margin: Margin, opacity: number, overflow: Overflow, padding: Padding, @@ -79,10 +86,14 @@ export default class NodeContainer { parent: ?NodeContainer; style: StyleDeclaration; childNodes: Array; + listItems: Array; + listIndex: ?number; + listStart: ?number; bounds: Bounds; curvedBounds: BoundCurves; image: ?string; index: number; + tagName: string; constructor( node: HTMLElement | SVGSVGElement, @@ -91,8 +102,13 @@ export default class NodeContainer { index: number ) { this.parent = parent; + this.tagName = node.tagName; this.index = index; this.childNodes = []; + this.listItems = []; + if (typeof node.start === 'number') { + this.listStart = node.start; + } const defaultView = node.ownerDocument.defaultView; const scrollX = defaultView.pageXOffset; const scrollY = defaultView.pageYOffset; @@ -117,6 +133,8 @@ export default class NodeContainer { float: parseCSSFloat(style.float), font: parseFont(style), letterSpacing: parseLetterSpacing(style.letterSpacing), + listStyle: display === DISPLAY.LIST_ITEM ? parseListStyle(style) : null, + margin: parseMargin(style), opacity: parseFloat(style.opacity), overflow: INPUT_TAGS.indexOf(node.tagName) === -1 @@ -137,6 +155,20 @@ export default class NodeContainer { node.style.transform = 'matrix(1,0,0,1,0,0)'; } + if (display === DISPLAY.LIST_ITEM) { + const listOwner = getListOwner(this); + if (listOwner) { + const listIndex = listOwner.listItems.length; + listOwner.listItems.push(this); + this.listIndex = + node.hasAttribute('value') && typeof node.value === 'number' + ? node.value + : listIndex === 0 + ? typeof listOwner.listStart === 'number' ? listOwner.listStart : 1 + : listOwner.listItems[listIndex - 1].listIndex + 1; + } + } + // TODO move bound retrieval for all nodes to a later stage? if (node.tagName === 'IMG') { node.addEventListener('load', () => { diff --git a/src/NodeParser.js b/src/NodeParser.js index 825f1e1..cb3772f 100644 --- a/src/NodeParser.js +++ b/src/NodeParser.js @@ -72,8 +72,10 @@ const parseNodeTree = ( } else if (childNode.tagName === 'SELECT') { // $FlowFixMe inlineSelectElement(childNode, container); - } else if (childNode.tagName === 'LI') { - // $FlowFixMe + } else if ( + container.style.listStyle && + container.style.listStyle.listStyleType !== 'none' + ) { inlineListItemElement(childNode, container, resourceLoader); } diff --git a/src/Unicode.js b/src/Unicode.js new file mode 100644 index 0000000..d2364ff --- /dev/null +++ b/src/Unicode.js @@ -0,0 +1,32 @@ +/* @flow */ +'use strict'; + +export const fromCodePoint = (...codePoints: Array): string => { + if (String.fromCodePoint) { + return String.fromCodePoint(...codePoints); + } + + const length = codePoints.length; + if (!length) { + return ''; + } + + const codeUnits = []; + + let index = -1; + let result = ''; + while (++index < length) { + let codePoint = codePoints[index]; + if (codePoint <= 0xffff) { + codeUnits.push(codePoint); + } else { + codePoint -= 0x10000; + codeUnits.push((codePoint >> 10) + 0xd800, codePoint % 0x400 + 0xdc00); + } + if (index + 1 === length || codeUnits.length > 0x4000) { + result += String.fromCharCode(...codeUnits); + codeUnits.length = 0; + } + } + return result; +}; diff --git a/src/Util.js b/src/Util.js index 44a1a13..7d390e8 100644 --- a/src/Util.js +++ b/src/Util.js @@ -17,23 +17,5 @@ export const copyCSSStyles = (style: CSSStyleDeclaration, target: HTMLElement): return target; }; -export const getParentOfType = (node: HTMLElement, parentTypes: Array): ?HTMLElement => { - let parent = node.parentNode; - if (!parent) { - return null; - } - - // $FlowFixMe - while (parentTypes.indexOf(parent.tagName) < 0) { - parent = parent.parentNode; - if (!parent) { - return null; - } - } - - // $FlowFixMe - return parent; -}; - export const SMALL_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; diff --git a/src/parsing/listStyle.js b/src/parsing/listStyle.js new file mode 100644 index 0000000..f9d2c14 --- /dev/null +++ b/src/parsing/listStyle.js @@ -0,0 +1,209 @@ +/* @flow */ +'use strict'; + +import type {BackgroundSource} from './background'; +import {parseBackgroundImage} from './background'; + +export type ListStyle = { + listStyleType: ListStyleType, + listStyleImage: ?BackgroundSource, + listStylePosition: ListStylePosition +}; + +export const LIST_STYLE_POSITION = { + INSIDE: 0, + OUTSIDE: 1 +}; + +export const LIST_STYLE_TYPE = { + NONE: -1, + DISC: 0, + CIRCLE: 1, + SQUARE: 2, + DECIMAL: 3, + CJK_DECIMAL: 4, + DECIMAL_LEADING_ZERO: 5, + LOWER_ROMAN: 6, + UPPER_ROMAN: 7, + LOWER_GREEK: 8, + LOWER_ALPHA: 9, + UPPER_ALPHA: 10, + ARABIC_INDIC: 11, + ARMENIAN: 12, + BENGALI: 13, + CAMBODIAN: 14, + CJK_EARTHLY_BRANCH: 15, + CJK_HEAVENLY_STEM: 16, + CJK_IDEOGRAPHIC: 17, + DEVANAGARI: 18, + ETHIOPIC_NUMERIC: 19, + GEORGIAN: 20, + GUJARATI: 21, + GURMUKHI: 22, + HEBREW: 22, + HIRAGANA: 23, + HIRAGANA_IROHA: 24, + JAPANESE_FORMAL: 25, + JAPANESE_INFORMAL: 26, + KANNADA: 27, + KATAKANA: 28, + KATAKANA_IROHA: 29, + KHMER: 30, + KOREAN_HANGUL_FORMAL: 31, + KOREAN_HANJA_FORMAL: 32, + KOREAN_HANJA_INFORMAL: 33, + LAO: 34, + LOWER_ARMENIAN: 35, + MALAYALAM: 36, + MONGOLIAN: 37, + MYANMAR: 38, + ORIYA: 39, + PERSIAN: 40, + SIMP_CHINESE_FORMAL: 41, + SIMP_CHINESE_INFORMAL: 42, + TAMIL: 43, + TELUGU: 44, + THAI: 45, + TIBETAN: 46, + TRAD_CHINESE_FORMAL: 47, + TRAD_CHINESE_INFORMAL: 48, + UPPER_ARMENIAN: 49, + DISCLOSURE_OPEN: 50, + DISCLOSURE_CLOSED: 51 +}; + +export type ListStylePosition = $Values; +export type ListStyleType = $Values; + +const parseListStyleType = (type: string): ListStyleType => { + switch (type) { + case 'disc': + return LIST_STYLE_TYPE.DISC; + case 'circle': + return LIST_STYLE_TYPE.CIRCLE; + case 'square': + return LIST_STYLE_TYPE.SQUARE; + case 'decimal': + return LIST_STYLE_TYPE.DECIMAL; + case 'cjk-decimal': + return LIST_STYLE_TYPE.CJK_DECIMAL; + case 'decimal-leading-zero': + return LIST_STYLE_TYPE.DECIMAL_LEADING_ZERO; + case 'lower-roman': + return LIST_STYLE_TYPE.LOWER_ROMAN; + case 'upper-roman': + return LIST_STYLE_TYPE.UPPER_ROMAN; + case 'lower-greek': + return LIST_STYLE_TYPE.LOWER_GREEK; + case 'lower-alpha': + return LIST_STYLE_TYPE.LOWER_ALPHA; + case 'upper-alpha': + return LIST_STYLE_TYPE.UPPER_ALPHA; + case 'arabic-indic': + return LIST_STYLE_TYPE.ARABIC_INDIC; + case 'armenian': + return LIST_STYLE_TYPE.ARMENIAN; + case 'bengali': + return LIST_STYLE_TYPE.BENGALI; + case 'cambodian': + return LIST_STYLE_TYPE.CAMBODIAN; + case 'cjk-earthly-branch': + return LIST_STYLE_TYPE.CJK_EARTHLY_BRANCH; + case 'cjk-heavenly-stem': + return LIST_STYLE_TYPE.CJK_HEAVENLY_STEM; + case 'cjk-ideographic': + return LIST_STYLE_TYPE.CJK_IDEOGRAPHIC; + case 'devanagari': + return LIST_STYLE_TYPE.DEVANAGARI; + case 'ethiopic-numeric': + return LIST_STYLE_TYPE.ETHIOPIC_NUMERIC; + case 'georgian': + return LIST_STYLE_TYPE.GEORGIAN; + case 'gujarati': + return LIST_STYLE_TYPE.GUJARATI; + case 'gurmukhi': + return LIST_STYLE_TYPE.GURMUKHI; + case 'hebrew': + return LIST_STYLE_TYPE.HEBREW; + case 'hiragana': + return LIST_STYLE_TYPE.HIRAGANA; + case 'hiragana-iroha': + return LIST_STYLE_TYPE.HIRAGANA_IROHA; + case 'japanese-formal': + return LIST_STYLE_TYPE.JAPANESE_FORMAL; + case 'japanese-informal': + return LIST_STYLE_TYPE.JAPANESE_INFORMAL; + case 'kannada': + return LIST_STYLE_TYPE.KANNADA; + case 'katakana': + return LIST_STYLE_TYPE.KATAKANA; + case 'katakana-iroha': + return LIST_STYLE_TYPE.KATAKANA_IROHA; + case 'khmer': + return LIST_STYLE_TYPE.KHMER; + case 'korean-hangul-formal': + return LIST_STYLE_TYPE.KOREAN_HANGUL_FORMAL; + case 'korean-hanja-formal': + return LIST_STYLE_TYPE.KOREAN_HANJA_FORMAL; + case 'korean-hanja-informal': + return LIST_STYLE_TYPE.KOREAN_HANJA_INFORMAL; + case 'lao': + return LIST_STYLE_TYPE.LAO; + case 'lower-armenian': + return LIST_STYLE_TYPE.LOWER_ARMENIAN; + case 'malayalam': + return LIST_STYLE_TYPE.MALAYALAM; + case 'mongolian': + return LIST_STYLE_TYPE.MONGOLIAN; + case 'myanmar': + return LIST_STYLE_TYPE.MYANMAR; + case 'oriya': + return LIST_STYLE_TYPE.ORIYA; + case 'persian': + return LIST_STYLE_TYPE.PERSIAN; + case 'simp-chinese-formal': + return LIST_STYLE_TYPE.SIMP_CHINESE_FORMAL; + case 'simp-chinese-informal': + return LIST_STYLE_TYPE.SIMP_CHINESE_INFORMAL; + case 'tamil': + return LIST_STYLE_TYPE.TAMIL; + case 'telugu': + return LIST_STYLE_TYPE.TELUGU; + case 'thai': + return LIST_STYLE_TYPE.THAI; + case 'tibetan': + return LIST_STYLE_TYPE.TIBETAN; + case 'trad-chinese-formal': + return LIST_STYLE_TYPE.TRAD_CHINESE_FORMAL; + case 'trad-chinese-informal': + return LIST_STYLE_TYPE.TRAD_CHINESE_INFORMAL; + case 'upper-armenian': + return LIST_STYLE_TYPE.UPPER_ARMENIAN; + case 'disclosure-open': + return LIST_STYLE_TYPE.DISCLOSURE_OPEN; + case 'disclosure-closed': + return LIST_STYLE_TYPE.DISCLOSURE_CLOSED; + case 'none': + default: + return LIST_STYLE_TYPE.NONE; + } +}; + +export const parseListStyle = (style: CSSStyleDeclaration): ListStyle => { + const listStyleImage = parseBackgroundImage(style.getPropertyValue('list-style-image')); + return { + listStyleType: parseListStyleType(style.getPropertyValue('list-style-type')), + listStyleImage: listStyleImage.length ? listStyleImage[0] : null, + listStylePosition: parseListStylePosition(style.getPropertyValue('list-style-position')) + }; +}; + +const parseListStylePosition = (position: string): ListStylePosition => { + switch (position) { + case 'inside': + return LIST_STYLE_POSITION.INSIDE; + case 'outside': + default: + return LIST_STYLE_POSITION.OUTSIDE; + } +}; diff --git a/src/parsing/margin.js b/src/parsing/margin.js new file mode 100644 index 0000000..91445d1 --- /dev/null +++ b/src/parsing/margin.js @@ -0,0 +1,11 @@ +/* @flow */ +'use strict'; +import Length from '../Length'; + +const SIDES = ['top', 'right', 'bottom', 'left']; + +export type Margin = Array; + +export const parseMargin = (style: CSSStyleDeclaration): Margin => { + return SIDES.map(side => new Length(style.getPropertyValue(`margin-${side}`))); +}; diff --git a/tests/reftests/list/liststyle.html b/tests/reftests/list/liststyle.html index 7683242..9bf82d0 100644 --- a/tests/reftests/list/liststyle.html +++ b/tests/reftests/list/liststyle.html @@ -6,6 +6,9 @@ @@ -40,7 +59,7 @@
  • Alpha
  • Beta
  • Gamma
  • - +
    • Alpha
    • Beta
    • @@ -71,5 +90,13 @@
    • Beta
    • Gamma
    • +
        +
      1. Alpha
      2. +
      3. Beta
      4. +
      5. Gamma
      6. +
      +
      Alpha
      +
      Beta
      +
      Gamma
      - \ No newline at end of file +