/* Create a value node. If the value is an XML node already, we are done. Otherwise, cast the value to a string and create a text child node containing the string value. */ static EjsXML *createValueNode(Ejs *ejs, EjsXML *elt, EjsObj *value) { EjsXML *text; EjsString *str; if (ejsIsXML(ejs, value)) { return (EjsXML*) value; } if ((str = (EjsString*) ejsCast(ejs, value, String)) == NULL) { return 0; } if (mprGetListLength(elt->elements) == 1) { /* Update an existing text element */ text = mprGetFirstItem(elt->elements); if (text->kind == EJS_XML_TEXT) { text->value = str; return elt; } } /* Create a new text element */ if (str->value[0] != '\0') { text = ejsCreateXML(ejs, EJS_XML_TEXT, N(NULL, NULL), elt, str); elt = ejsAppendToXML(ejs, elt, text); } return elt; }
/* native function XML(value: Object = null) */ static EjsObj *xmlConstructor(Ejs *ejs, EjsXML *thisObj, int argc, EjsObj **argv) { EjsObj *arg, *vp; wchar *str; // TODO - should be also able to handle a File object if (thisObj == 0) { /* Called as a function - cast the arg */ if (argc > 0) { if ((arg = ejsCast(ejs, argv[0], String)) == 0) { return 0; } } thisObj = ejsCreateXML(ejs, 0, N(NULL, NULL), NULL, NULL); } if (argc == 0) { return (EjsObj*) thisObj; } arg = argv[0]; assert(arg); if (!ejsIsDefined(ejs, arg)) { return (EjsObj*) thisObj; } arg = ejsCast(ejs, argv[0], String); if (arg && ejsIs(ejs, arg, String)) { str = ((EjsString*) arg)->value; if (str == 0) { return 0; } while (isspace((uchar) *str)) str++; if (*str == '<') { /* XML Literal */ ejsLoadXMLString(ejs, thisObj, (EjsString*) arg); } else { /* Load from file */ loadXml(ejs, thisObj, argc, argv); } } else if (arg && ejsIsXML(ejs, arg)) { if ((vp = xmlToString(ejs, argv[0], 0, NULL)) != 0) { ejsLoadXMLString(ejs, thisObj, (EjsString*) vp); } } else { ejsThrowArgError(ejs, "Bad type passed to XML constructor"); return 0; } return (EjsObj*) thisObj; }
/* Update an existing element */ static int updateElement(Ejs *ejs, EjsXML *list, EjsXML *elt, int index, EjsObj *value) { EjsXML *node; int i, j; if (!ejsIsXML(ejs, value)) { /* Not XML or XMLList -- convert to string */ value = ejsCast(ejs, value, String); // TODO - seem to be doing this in too many places } mprSetItem(list->elements, index, value); if (elt->kind == EJS_XML_ATTRIBUTE) { assure(ejsIs(ejs, value, String)); i = mprLookupItem(elt->parent->elements, elt); assure(i >= 0); ejsSetXMLElement(ejs, elt->parent, i, elt); // TODO - why do this. Doesn't above do this? ejsSetPropertyByName(ejs, elt->parent, elt->qname, value); elt->value = (EjsString*) value; } if (ejsIsXML(ejs, value) && ((EjsXML*) value)->kind == EJS_XML_LIST) { value = (EjsObj*) shallowCopy(ejs, (EjsXML*) value); if (elt->parent) { index = mprLookupItem(elt->parent->elements, elt); assure(index >= 0); for (j = 0; j < mprGetListLength(((EjsXML*) value)->elements); j++) { mprInsertItemAtPos(elt->parent->elements, index, value); } } } else if (ejsIsXML(ejs, value) || elt->kind != EJS_XML_ELEMENT) { if (elt->parent) { index = mprLookupItem(elt->parent->elements, elt); assure(index >= 0); mprSetItem(elt->parent->elements, index, value); ((EjsXML*) value)->parent = elt->parent; if (ejsIs(ejs, value, String)) { node = ejsCreateXML(ejs, EJS_XML_TEXT, N(NULL, NULL), list, (EjsString*) value); mprSetItem(list->elements, index, node); } else { mprSetItem(list->elements, index, value); } } } else { ejsSetPropertyByName(ejs, elt, N(NULL, "*"), value); } return index; }
PUBLIC EjsAny *cloneXml(Ejs *ejs, EjsXML *xml, bool deep) { EjsXML *root, *elt; int next; if (xml == 0) { return 0; } if (xml->kind == EJS_XML_LIST) { root = ejsCreateXMLList(ejs, xml->targetObject, xml->targetProperty); } else { root = ejsCreateXML(ejs, xml->kind, xml->qname, NULL, xml->value); } if (root == 0) { return 0; } // TODO - must copy inScopeNamespaces? if (xml->attributes) { root->attributes = mprCreateList(-1, 0); for (next = 0; (elt = (EjsXML*) mprGetNextItem(xml->attributes, &next)) != 0; ) { elt = ejsClone(ejs, elt, 1); if (elt) { elt->parent = root; mprAddItem(root->attributes, elt); } } } if (xml->elements) { root->elements = mprCreateList(-1, 0); for (next = 0; (elt = mprGetNextItem(xml->elements, &next)) != 0; ) { assert(ejsIsXML(ejs, elt)); elt = ejsClone(ejs, elt, 1); if (elt) { elt->parent = root; mprAddItem(root->elements, elt); } } } if (mprHasMemError(ejs)) { return 0; } return root; }
static int parserHandler(MprXml *xp, int state, cchar *tagName, cchar *attName, cchar *value) { Ejs *ejs; EjsXmlState *parser; EjsXmlTagState *tos; EjsName qname; EjsXML *xml, *node, *parent; parser = (EjsXmlState*) xp->parseArg; ejs = parser->ejs; tos = &parser->nodeStack[parser->topOfStack]; xml = tos->obj; mprAssert(xml); mprAssert(state >= 0); mprAssert(tagName && *tagName); switch (state) { case MPR_XML_PI: node = ejsCreateXML(ejs, EJS_XML_PROCESSING, NULL, xml, value); ejsAppendToXML(ejs, xml, node); break; case MPR_XML_COMMENT: node = ejsCreateXML(ejs, EJS_XML_COMMENT, NULL, xml, value); ejsAppendToXML(ejs, xml, node); break; case MPR_XML_NEW_ELT: if (parser->topOfStack > E4X_MAX_NODE_DEPTH) { ejsThrowSyntaxError(ejs, "XML nodes nested too deeply in %s at line %d", parser->filename, mprXmlGetLineNumber(xp)); return MPR_ERR_BAD_SYNTAX; } if (xml->kind <= 0) { ejsConfigureXML(ejs, xml, EJS_XML_ELEMENT, tagName, xml, NULL); } else { ejsName(&qname, 0, tagName); xml = ejsCreateXML(ejs, EJS_XML_ELEMENT, &qname, xml, NULL); tos = &parser->nodeStack[++(parser->topOfStack)]; tos->obj = (EjsXML*) xml; tos->attributes = 0; tos->comments = 0; } break; case MPR_XML_NEW_ATT: ejsName(&qname, 0, attName); node = ejsCreateXML(ejs, EJS_XML_ATTRIBUTE, &qname, xml, value); ejsAppendAttributeToXML(ejs, xml, node); break; case MPR_XML_SOLO_ELT_DEFINED: if (parser->topOfStack > 0) { parent = parser->nodeStack[parser->topOfStack - 1].obj; ejsAppendToXML(ejs, parent, xml); parser->topOfStack--; mprAssert(parser->topOfStack >= 0); tos = &parser->nodeStack[parser->topOfStack]; } break; case MPR_XML_ELT_DEFINED: if (parser->topOfStack > 0) { parent = parser->nodeStack[parser->topOfStack - 1].obj; ejsAppendToXML(ejs, parent, xml); } break; case MPR_XML_ELT_DATA: case MPR_XML_CDATA: ejsName(&qname, 0, attName); node = ejsCreateXML(ejs, EJS_XML_TEXT, &qname, xml, value); ejsAppendToXML(ejs, xml, node); break; case MPR_XML_END_ELT: /* * This is the closing element in a pair "<x>...</x>". * Pop the stack frame off the elt stack */ if (parser->topOfStack > 0) { parser->topOfStack--; mprAssert(parser->topOfStack >= 0); tos = &parser->nodeStack[parser->topOfStack]; } break; default: ejsThrowSyntaxError(ejs, "XML error in %s at %d\nDetails %s", parser->filename, mprXmlGetLineNumber(xp), mprXmlGetErrorMsg(xp)); mprAssert(0); return MPR_ERR_BAD_SYNTAX; } return 0; }
/* Set a property by name There are 7 kinds of qname's: prop, @att, [prop], *, @*, .name, .@name */ static int setXmlPropertyByName(Ejs *ejs, EjsXML *xml, EjsName qname, EjsObj *value) { EjsXML *elt, *xvalue, *rp, *lastElt; EjsObj *originalValue; int index, last; last = 0; lastElt = 0; if (isdigit((uchar) qname.name->value[0]) && allDigitsForXml(qname.name)) { ejsThrowTypeError(ejs, "Integer indicies for set are not allowed"); return EJS_ERR; } if (xml->kind != EJS_XML_ELEMENT) { // TODO spec requires this -- but why? -- surely throw? return 0; } /* Massage the value type. */ originalValue = value; xvalue = (EjsXML*) value; if (ejsIsXML(ejs, xvalue)) { if (xvalue->kind == EJS_XML_LIST) { value = cloneXml(ejs, xvalue, 1); } else if (xvalue->kind == EJS_XML_TEXT || xvalue->kind == EJS_XML_ATTRIBUTE) { value = ejsCast(ejs, originalValue, String); } else { value = cloneXml(ejs, xvalue, 1); } } else { value = ejsCast(ejs, value, String); } if (qname.name->value[0] == '@') { return setXmlPropertyAttributeByName(ejs, xml, qname, value); } /* Delete redundant elements by the same name. */ if (xml->elements) { for (last = -1, index = -1; (elt = mprGetPrevItem(xml->elements, &index)) != 0; ) { if (qname.name->value[0] == '*' || (elt->kind == EJS_XML_ELEMENT && elt->qname.name == qname.name)) { /* Must remove all redundant elements of the same name except the first one */ if (last >= 0) { rp = mprGetItem(xml->elements, last); rp->parent = 0; mprRemoveItemAtPos(xml->elements, last); } last = index; lastElt = elt; } } } if (xml->elements == 0) { // TODO - need routine to do this centrally so we can control the default number of elements in the list? xml->elements = mprCreateList(-1, 0); } elt = lastElt; index = last; if (qname.name->value[0] == '*') { /* Special case when called from XMLList to update the value of an element */ xml = createValueNode(ejs, xml, value); } else if (elt == 0) { /* Not found. New node required. */ elt = ejsCreateXML(ejs, EJS_XML_ELEMENT, qname, xml, NULL); if (elt == 0) { return 0; } index = mprGetListLength(xml->elements); xml = ejsAppendToXML(ejs, xml, createValueNode(ejs, elt, value)); } else { /* Update existing element. */ xml = ejsSetXMLElement(ejs, xml, index, createValueNode(ejs, elt, value)); } if (xml == 0) { return EJS_ERR; } return index; }
/* Set a property attribute by name. */ static int setXmlPropertyAttributeByName(Ejs *ejs, EjsXML *xml, EjsName qname, EjsObj *value) { EjsXML *elt, *attribute, *xvalue, *lastElt; EjsString *sv; EjsName qn; wchar *str; int index, last, next; /* Attribute. If the value is an XML list, convert to a space separated string */ xvalue = (EjsXML*) value; if (ejsIsXML(ejs, xvalue) && xvalue->kind == EJS_XML_LIST) { str = 0; for (next = 0; (elt = mprGetNextItem(xvalue->elements, &next)) != 0; ) { sv = (EjsString*) ejsCast(ejs, (EjsObj*) elt, String); str = mrejoin(str, NULL, " ", sv->value, NULL); } value = (EjsObj*) ejsCreateString(ejs, str, -1); } else { value = ejsCast(ejs, value, String); } assert(ejsIs(ejs, value, String)); /* Find the first attribute that matches. Delete all other attributes of the same name. */ index = 0; if (xml->attributes) { lastElt = 0; for (last = -1, index = -1; (elt = mprGetPrevItem(xml->attributes, &index)) != 0; ) { assert(qname.name->value[0] == '@'); if (wcmp(elt->qname.name->value, &qname.name->value[1]) == 0) { if (last >= 0) { mprRemoveItemAtPos(xml->attributes, last); } last = index; lastElt = elt; } } if (lastElt) { /* Found a match. So replace its value */ lastElt->value = (EjsString*) value; return last; } else { index = mprGetListLength(xml->attributes); } } // TODO - namespace work to do here /* Not found. Create a new attribute node */ assert(ejsIs(ejs, value, String)); qn.space = NULL; qn.name = ejsSubstring(ejs, qname.name, 1, -1); attribute = ejsCreateXML(ejs, EJS_XML_ATTRIBUTE, qn, xml, (EjsString*) value); if (xml->attributes == 0) { xml->attributes = mprCreateList(-1, 0); } mprSetItem(xml->attributes, index, attribute); return index; }
static EjsXML *createXml(Ejs *ejs, EjsType *type, int size) { return ejsCreateXML(ejs, 0, N(NULL, NULL), NULL, NULL); }
static EjsXML *createElement(Ejs *ejs, EjsXML *list, EjsXML *targetObject, EjsName qname, EjsObj *value) { EjsXML *elt, *last, *attList; int index; int j; if (targetObject && ejsIsXML(ejs, targetObject) && targetObject->kind == EJS_XML_LIST) { /* If the target is a list it must have 1 element. So switch to it. TODO - could we get resolve to do this? */ if (mprGetListLength(targetObject->elements) != 1) { /* Spec says so - TODO why no error? */ return 0; } targetObject = mprGetFirstItem(targetObject->elements); } /* Return if the target object is not an XML element */ if (!ejsIsXML(ejs, targetObject) || targetObject->kind != EJS_XML_ELEMENT) { /* Spec says so - TODO why no error? */ return 0; } elt = ejsCreateXML(ejs, EJS_XML_ELEMENT, list->targetProperty, targetObject, NULL); if (list->targetProperty.name && list->targetProperty.name->value[0] == '@') { elt->kind = EJS_XML_ATTRIBUTE; attList = ejsGetPropertyByName(ejs, (EjsObj*) targetObject, list->targetProperty); if (attList && mprGetListLength(attList->elements) > 0) { /* Spec says so. But this surely means you can't update an attribute? */ return 0; } } else if (list->targetProperty.name == NULL || qname.name->value[0] == '*') { elt->kind = EJS_XML_TEXT; elt->qname.name = 0; } index = mprGetListLength(list->elements); if (elt->kind != EJS_XML_ATTRIBUTE) { if (targetObject) { if (index > 0) { /* Find the place of the last list item in the resolved target object. */ last = mprGetItem(list->elements, index - 1); j = mprLookupItem(targetObject->elements, last); } else { j = -1; } if (j < 0) { j = mprGetListLength(targetObject->elements) - 1; } // TODO - really need to wrap this ejsInsertXML(EjsXML *xml, int index, EjsXML *node) if (targetObject->elements == 0) { targetObject->elements = mprCreateList(-1, 0); } /* Insert into the target object */ mprInsertItemAtPos(targetObject->elements, j + 1, elt); } if (ejsIsXML(ejs, value)) { if (((EjsXML*) value)->kind == EJS_XML_LIST) { elt->qname = ((EjsXML*) value)->targetProperty; } else { elt->qname = ((EjsXML*) value)->qname; } } /* Insert into the XML list */ mprSetItem(list->elements, index, elt); } return (EjsXML*) mprGetItem(list->elements, index); }