blob: e9dee43742c5cf7a67b1e564f76d244d9d994d82 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#include "DFPlatform.h"
#include "WordLists.h"
#include "DFDOM.h"
#include "CSSProperties.h"
#include "CSSLength.h"
#include "WordConverter.h"
#include "WordNumbering.h"
#include "WordStyles.h"
#include "DFHTML.h"
#include "DFString.h"
#include "DFCommon.h"
#include <assert.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>
typedef struct ListDimensions {
double marginLeftPct;
double textIndentPct;
double totalPct;
} ListDimensions;
static ListDimensions ListDimensionsZero = { 0, 0, 0 };
////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// ListFrame //
// //
////////////////////////////////////////////////////////////////////////////////////////////////////
typedef struct ListFrame ListFrame;
struct ListFrame {
DFNode *element;
ListFrame *parent;
int numId;
int ilvl;
ListDimensions dimensions;
};
ListFrame *ListFrameNew(DFNode *element, ListFrame *parent, int numId, int ilvl, ListDimensions dimensions)
{
ListFrame *frame = (ListFrame *)xcalloc(1,sizeof(ListFrame));
frame->element = element;
frame->parent = parent;
frame->numId = numId;
frame->ilvl = ilvl;
frame->dimensions = dimensions;
return frame;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// ListStack //
// //
////////////////////////////////////////////////////////////////////////////////////////////////////
typedef struct {
ListFrame *top;
} ListStack;
static ListFrame *ListStackPushFrame(ListStack *stack, DFNode *element, int numId, int ilvl, ListDimensions dimensions)
{
stack->top = ListFrameNew(element, stack->top, numId, ilvl, dimensions);
return stack->top;
}
static void ListStackPop(ListStack *stack)
{
if (stack->top != NULL) {
ListFrame *oldTop = stack->top;
stack->top = stack->top->parent;
free(oldTop);
}
}
static void ListStackPopToAboveIlvl(ListStack *stack, int ilvl)
{
while ((stack->top != NULL) && (stack->top->ilvl >= ilvl))
ListStackPop(stack);
}
//@end
////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// WordLists //
// //
////////////////////////////////////////////////////////////////////////////////////////////////////
static ListDimensions cssPropertiesIndent(CSSProperties *properties)
{
ListDimensions result;
result.marginLeftPct = 0;
result.textIndentPct = 0;
result.totalPct = 0;
if (CSSGet(properties,"margin-left") != NULL) {
CSSLength length = CSSLengthFromString(CSSGet(properties,"margin-left"));
if (CSSLengthIsValid(length) && (length.units == UnitsPct))
result.marginLeftPct = length.value;
}
if (CSSGet(properties,"text-indent") != NULL) {
CSSLength length = CSSLengthFromString(CSSGet(properties,"text-indent"));
if (CSSLengthIsValid(length) && (length.units == UnitsPct))
result.textIndentPct = length.value;
}
result.totalPct = result.marginLeftPct + result.textIndentPct;
return result;
}
static int listProperties(WordConverter *conv, const char *numId, const char *ilvl, CSSProperties *properties)
{
WordConcreteNum *num = WordNumberingConcreteWithId(conv->numbering,numId);
if (num == NULL)
return 0;;
WordNumLevel *level = WordConcreteNumGetLevel(num,atoi(ilvl));
if (level == NULL)
return 0;;
DFNode *pPr = DFChildWithTag(level->element,WORD_PPR);
if (pPr == NULL)
return 0;;
const char *styleId = NULL;
WordGetPPr(pPr,properties,&styleId,conv->mainSection);
return 1;
}
static ListDimensions listIndent(WordConverter *conv, const char *numId, const char *ilvl)
{
CSSProperties *properties = CSSPropertiesNew();
listProperties(conv,numId,ilvl,properties);
ListDimensions result = cssPropertiesIndent(properties);
CSSPropertiesRelease(properties);
return result;
}
// FIXME: make private to this file
double listDesiredIndent(WordConverter *conv, const char *numId, const char *ilvl)
{
CSSProperties *properties = CSSPropertiesNew();
listProperties(conv,numId,ilvl,properties);
double result = 0.0;
if (CSSGet(properties,"margin-left") != NULL) {
CSSLength length = CSSLengthFromString(CSSGet(properties,"margin-left"));
if (CSSLengthIsValid(length) && (length.units == UnitsPct))
result += length.value;
}
CSSPropertiesRelease(properties);
return result;
}
static ListDimensions paragraphIndent(DFNode *p)
{
if (p->tag < MIN_ELEMENT_TAG)
return ListDimensionsZero;;
const char *cssText = DFGetAttribute(p,HTML_STYLE);
CSSProperties *properties = CSSPropertiesNewWithString(cssText);
ListDimensions result = cssPropertiesIndent(properties);
CSSPropertiesRelease(properties);
return result;
}
static void adjustMarginLeft(DFNode *element, double adjustPct, int noTextIndent)
{
if ((element->tag != HTML_TABLE) && !HTML_isParagraphTag(element->tag))
return;;
const char *cssText = DFGetAttribute(element,HTML_STYLE);
CSSProperties *properties = CSSPropertiesNewWithString(cssText);
double oldMarginLeft = 0;
if (CSSGet(properties,"margin-left") != NULL) {
CSSLength length = CSSLengthFromString(CSSGet(properties,"margin-left"));
if (CSSLengthIsValid(length) && (length.units == UnitsPct))
oldMarginLeft = length.value;
if (CSSGet(properties,"width") != NULL) {
CSSLength length = CSSLengthFromString(CSSGet(properties,"width"));
if (CSSLengthIsValid(length) && (length.units == UnitsPct)) {
double oldWidth = length.value;
double newWidth = oldWidth + oldMarginLeft;
char buf[100];
CSSPut(properties,"width",DFFormatDoublePct(buf,100,newWidth));
}
}
}
double oldTextIndent = 0;
if (CSSGet(properties,"text-indent") != NULL) {
CSSLength length = CSSLengthFromString(CSSGet(properties,"text-indent"));
if (CSSLengthIsValid(length) && (length.units == UnitsPct))
oldTextIndent = length.value;
}
double newMarginLeft = oldMarginLeft + adjustPct;
double newTextIndent = oldTextIndent;
if (newMarginLeft < 0)
newMarginLeft = 0;
if (fabs(newMarginLeft) >= 0.01) {
char buf[100];
CSSPut(properties,"margin-left",DFFormatDoublePct(buf,100,newMarginLeft));
}
else {
CSSPut(properties,"margin-left",NULL);
}
if (noTextIndent) {
CSSPut(properties,"text-indent",NULL);
}
else if (newTextIndent < -newMarginLeft) {
// Don't allow negative text-indent
newTextIndent = -newMarginLeft;
if (fabs(newTextIndent) >= 0.01) {
char buf[100];
CSSPut(properties,"text-indent",DFFormatDoublePct(buf,100,newTextIndent));
}
else {
CSSPut(properties,"text-indent",NULL);
}
}
char *propertiesText = CSSPropertiesCopyDescription(properties);
if (strlen(propertiesText) == 0)
DFRemoveAttribute(element,HTML_STYLE);
else
DFSetAttribute(element,HTML_STYLE,propertiesText);
free(propertiesText);
CSSPropertiesRelease(properties);
}
static int isEmptyParagraph(DFNode *p)
{
if (p->tag != HTML_P)
return 0;
for (DFNode *child = p->first; child != NULL; child = child->next) {
switch (child->tag) {
case HTML_BR:
break;
default:
return 0;
}
}
return 1;
}
static void fixTrailingParagraphs(ListStack *stack, int minIlvl)
{
if (stack->top == NULL)
return;;
DFNode *li = stack->top->element->last;
if (li == NULL)
return;;
DFNode *child = li->first;
// Move to first element
while ((child != NULL) && (child->tag < MIN_ELEMENT_TAG))
child = child->next;
if (child != NULL) {
// Adjust indentation of first element
if ((stack->top != NULL) && (child->tag >= MIN_ELEMENT_TAG)) {
double pct = stack->top->dimensions.marginLeftPct;
adjustMarginLeft(child,-pct,1);
}
// Skip past first element
child = child->next;
}
DFNode *preceding = (child != NULL) ? child->prev : NULL;
DFNode *parent = li;
while (child != NULL) {
ListDimensions dimensions = paragraphIndent(child);
if (!isEmptyParagraph(child) || (minIlvl < 0)) {
while ((stack->top != NULL) &&
(stack->top->ilvl >= minIlvl) &&
(dimensions.totalPct < stack->top->dimensions.totalPct - 0.001)) {
assert(stack->top != NULL);
preceding = stack->top->element;
parent = stack->top->element->parent;
ListStackPop(stack);
}
}
if ((stack->top != NULL) && (child->tag >= MIN_ELEMENT_TAG)) {
double pct = stack->top->dimensions.marginLeftPct;
adjustMarginLeft(child,-pct,0);
}
DFNode *next = child->next;
DFInsertBefore(parent,child,preceding ? preceding->next : NULL);
preceding = child;
child = next;
}
}
static void Word_fixListSingle(WordConverter *conv, DFNode *node)
{
ListStack stack;
bzero(&stack,sizeof(ListStack));
DFNode *next;
for (DFNode *child = node->first; child != NULL; child = next) {
next = child->next;
int isListItem = 0;
if (child->tag == HTML_P) {
DFNode *elem = child;
const char *numIdStr = DFGetAttribute(elem,WORD_NUMID);
const char *ilvlStr = DFGetAttribute(elem,WORD_ILVL);
DFRemoveAttribute(elem,WORD_NUMID);
DFRemoveAttribute(elem,WORD_ILVL);
// A numId of 0 means that there is no numbering applied to this paragraph
if ((numIdStr != NULL) && (atoi(numIdStr) == 0)) {
numIdStr = NULL;
ilvlStr = NULL;
}
if ((numIdStr != NULL) && (ilvlStr != NULL)) {
isListItem = 1;
int numId = atoi(numIdStr);
int ilvl = atoi(ilvlStr);
ListDimensions dimensions = listIndent(conv,numIdStr,ilvlStr);
// Find the list at the same ilvl, and check if it has the same numId. If not, we're
// starting a new list.
ListFrame *sameLevelFrame = NULL;
for (ListFrame *frame = stack.top; frame != NULL; frame = frame->parent) {
if (frame->ilvl == ilvl)
sameLevelFrame = frame;
}
if ((sameLevelFrame != NULL) && (sameLevelFrame->numId != numId))
fixTrailingParagraphs(&stack,ilvl);
else
fixTrailingParagraphs(&stack,ilvl+1);
if ((stack.top != NULL) && (stack.top->numId != numId))
ListStackPopToAboveIlvl(&stack,ilvl);
else if ((stack.top != NULL) && (stack.top->ilvl > ilvl))
ListStackPopToAboveIlvl(&stack,ilvl+1);
if ((stack.top == NULL) || (stack.top->numId != numId) || (stack.top->ilvl < ilvl)) {
WordConcreteNum *num = WordNumberingConcreteWithId(conv->numbering,numIdStr);
WordNumLevel *level = (num != NULL) ? WordConcreteNumGetLevel(num,ilvl) : NULL;
const char *type = WordNumLevelToListStyleType(level);
Tag tag;
if (DFStringEquals(type,"disc") ||
DFStringEquals(type,"circle") ||
DFStringEquals(type,"square"))
tag = HTML_UL;
else
tag = HTML_OL;
DFNode *element = DFCreateElement(conv->html,tag);
if (type != NULL)
DFFormatAttribute(element,HTML_STYLE,"list-style-type: %s",type);
if (stack.top != NULL) {
DFNode *li;
if (stack.top->element->last != NULL)
li = stack.top->element->last;
else
li = DFCreateChildElement(stack.top->element,HTML_LI);
DFAppendChild(li,element);
}
else {
DFInsertBefore(node,element,child);
}
ListStackPushFrame(&stack,element,numId,ilvl,dimensions);
}
}
}
if (stack.top != NULL) {
DFNode *li;
if ((stack.top->element->last != NULL) && !isListItem)
li = stack.top->element->last;
else
li = DFCreateChildElement(stack.top->element,HTML_LI);
DFAppendChild(li,child);
}
}
fixTrailingParagraphs(&stack,-1);
while (stack.top != NULL)
ListStackPop(&stack);
}
static void Word_fixLists(WordConverter *conv, DFNode *node)
{
int haveList = 0;
for (DFNode *child = node->first; child != NULL; child = child->next) {
if ((DFGetAttribute(child,WORD_NUMID) != NULL) &&
(DFGetAttribute(child,WORD_ILVL) != NULL)) {
haveList = 1;
break;
}
}
if (haveList)
Word_fixListSingle(conv,node);;
DFNode *next;
for (DFNode *child = node->first; child != NULL; child = next) {
next = child->next;
Word_fixLists(conv,child);
}
}
void WordPostProcessHTMLLists(WordConverter *conv)
{
Word_fixLists(conv,conv->html->docNode);
}
static void Word_preProcessLists(WordConverter *word, DFNode *node, int ilvl);
static void Word_flattenList(WordConverter *word, DFNode *list, int ilvl, DFNode *parent, DFNode *nextSibling)
{
const char *type = (list->tag == HTML_OL) ? "decimal" : "disc";
const char *cssText = DFGetAttribute(list,HTML_STYLE);
CSSProperties *properties = CSSPropertiesNewWithString(cssText);
if (CSSGet(properties,"list-style-type") != NULL)
type = CSSGet(properties,"list-style-type");
DFFormatAttribute(list,CONV_LISTNUM,"%u",list->seqNo);
DFFormatAttribute(list,CONV_ILVL,"%d",ilvl);
DFSetAttribute(list,CONV_LISTTYPE,type);
for (DFNode *li = list->first; li != NULL; li = li->next) {
DFNode *first = li->first;
DFNode *liChildNext;
for (DFNode *liChild = li->first; liChild != NULL; liChild = liChildNext) {
liChildNext = liChild->next;
if ((liChild->tag == HTML_UL) || (liChild->tag == HTML_OL)) {
Word_flattenList(word,liChild,ilvl+1,parent,nextSibling);
}
else {
if (liChild->tag == HTML_P) {
DFFormatAttribute(liChild,CONV_LISTNUM,"%u",list->seqNo);
DFFormatAttribute(liChild,CONV_ILVL,"%d",ilvl);
DFSetAttribute(liChild,CONV_LISTTYPE,type);
if (liChild == first)
DFSetAttribute(liChild,CONV_LISTITEM,"true");
}
DFInsertBefore(parent,liChild,nextSibling);
Word_preProcessLists(word,liChild,ilvl);
}
}
}
DFRemoveNode(list);
CSSPropertiesRelease(properties);
}
static void Word_preProcessLists(WordConverter *word, DFNode *node, int ilvl)
{
DFNode *next;
for (DFNode *child = node->first; child != NULL; child = next) {
next = child->next;
if ((child->tag == HTML_UL) || (child->tag == HTML_OL)) {
Word_flattenList(word,child,ilvl+1,node,next);
}
else {
Word_preProcessLists(word,child,ilvl);
}
}
}
void WordPreProcessHTMLLists(WordConverter *conv)
{
Word_preProcessLists(conv,conv->html->docNode,-1);
}
static void fixWordLists(DFNode *node, WordConverter *conv)
{
for (DFNode *child = node->first; child != NULL; child = child->next)
fixWordLists(child,conv);
int haveParagraphs = 0;
for (DFNode *child = node->first; child != NULL; child = child->next) {
if (child->tag == WORD_P) {
haveParagraphs = 1;
break;
}
}
if (!haveParagraphs)
return;
int createdHashTables = 0;
DFHashTable *replacementNumIds = NULL;
DFHashTable *itemNoByListKey = NULL;
DFHashTable *lastNumIdByIlvl = NULL;
DFHashTable *itemNoByIlvl = NULL;
int maxIlvl = -1;
for (DFNode *child = node->first; child != NULL; child = child->next) {
if (child->tag != WORD_P)
continue;
DFNode *pPrElem = DFChildWithTag(child,WORD_PPR);
DFNode *numPrElem = DFChildWithTag(pPrElem,WORD_NUMPR);
DFNode *numIdElem = DFChildWithTag(numPrElem,WORD_NUMID);
DFNode *ilvlElem = DFChildWithTag(numPrElem,WORD_ILVL);
const char *numId = DFGetAttribute(numIdElem,WORD_VAL);
const char *ilvl = DFGetAttribute(ilvlElem,WORD_VAL);
if ((numId == NULL) || (atoi(numId) == 0))
continue;
if (!createdHashTables) {
replacementNumIds = DFHashTableNew((DFCopyFunction)xstrdup,free);
itemNoByListKey = DFHashTableNew((DFCopyFunction)xstrdup,free);
lastNumIdByIlvl = DFHashTableNew((DFCopyFunction)xstrdup,free);
itemNoByIlvl = DFHashTableNew((DFCopyFunction)xstrdup,free);
createdHashTables = 1;
}
if (ilvl == NULL)
ilvl = "0";;
WordConcreteNum *concreteNum = WordNumberingConcreteWithId(conv->numbering,numId);
// FIXME: Crash here if concreteNum is NULL
WordNumLevel *numLevel = WordConcreteNumGetLevel(concreteNum,atoi(ilvl));
const char *levelStart = NULL;
if (numLevel != NULL) {
for (DFNode *lvlChild = numLevel->element->first; lvlChild != NULL; lvlChild = lvlChild->next) {
switch (lvlChild->tag) {
case WORD_START:
levelStart = DFGetAttribute(lvlChild,WORD_VAL);
break;
}
}
}
char *listKey = DFFormatString("%s:%s",numId,ilvl);
char *itemNo = DFStrDup(DFHashTableLookup(itemNoByListKey,listKey));
if (itemNo == NULL) {
itemNo = xstrdup("1");
if ((levelStart != NULL) && (atoi(levelStart) > 1) && (atoi(ilvl) <= maxIlvl)) {
const char *prevNumId = DFHashTableLookup(lastNumIdByIlvl,ilvl);
const char *prevItemNo = DFHashTableLookup(itemNoByIlvl,ilvl);
if ((prevNumId != NULL) && (prevItemNo != NULL)) {
DFHashTableAdd(replacementNumIds,numId,prevNumId);
free(itemNo);
itemNo = DFFormatString("%d",atoi(prevItemNo)+1);
}
}
}
else {
int intValue = atoi(itemNo);
free(itemNo);
itemNo = DFFormatString("%d",intValue+1);
}
DFHashTableAdd(itemNoByListKey,listKey,itemNo);
const char *replNumId = DFHashTableLookup(replacementNumIds,numId);
if (replNumId != NULL) {
numId = replNumId;
DFSetAttribute(numIdElem,WORD_VAL,numId);
}
DFHashTableAdd(lastNumIdByIlvl,ilvl,numId);
DFHashTableAdd(itemNoByIlvl,ilvl,itemNo);
maxIlvl = atoi(ilvl);
free(listKey);
free(itemNo);
}
DFHashTableRelease(replacementNumIds);
DFHashTableRelease(itemNoByListKey);
DFHashTableRelease(lastNumIdByIlvl);
DFHashTableRelease(itemNoByIlvl);
}
void WordFixLists(WordConverter *conv)
{
fixWordLists(conv->package->document->docNode,conv);
}