-- | This module provides for simple DOM traversal. It is inspired by XPath. There are two central concepts here:---- * A 'Cursor' represents a node in the DOM. It also contains information on the node's /location/. While the 'Node' datatype will only know of its children, a @Cursor@ knows about its parent and siblings as well. (The underlying mechanism allowing this is called a zipper, see <http://www.haskell.org/haskellwiki/Zipper> and <http://www.haskell.org/haskellwiki/Tying_the_Knot>.)---- * An 'Axis', in its simplest form, takes a @Cursor@ and returns a list of @Cursor@s. It is used for selections, such as finding children, ancestors, etc. Axes can be chained together to express complex rules, such as all children named /foo/.---- The terminology used in this module is taken directly from the XPath-- specification: <http://www.w3.org/TR/xpath/>. For those familiar with XPath,-- the one major difference is that attributes are not considered nodes in this-- module.moduleText.XML.Cursor(-- * Data typesCursor,Axis-- * Production,fromDocument,fromNode,cut-- * Axes,parent,CG.precedingSibling,CG.followingSibling,child,node,CG.preceding,CG.following,CG.ancestor,descendant,orSelf-- ** Filters,check,checkNode,checkElement,checkName,anyElement,element,laxElement,content,attribute,laxAttribute,hasAttribute,attributeIs-- * Operators,(CG.&|),(CG.&/),(CG.&//),(CG.&.//),(CG.$|),(CG.$/),(CG.$//),(CG.$.//),(CG.>=>)-- * Type classes,Boolean(..)-- * Error handling,force,forceM)whereimportControl.MonadimportData.Function(on)importText.XMLimportqualifiedControl.FailureasFimportqualifiedData.TextasTimportqualifiedData.MapasMapimportqualifiedText.XML.Cursor.GenericasCGimportText.XML.Cursor.Generic(node,child,parent,descendant,orSelf)importData.Maybe(maybeToList)-- TODO: Consider [Cursor] -> [Cursor]?-- | The type of an Axis that returns a list of Cursors.-- They are roughly modeled after <http://www.w3.org/TR/xpath/#axes>.-- -- Axes can be composed with '>=>', where e.g. @f >=> g@ means that on all results of-- the @f@ axis, the @g@ axis will be applied, and all results joined together. -- Because Axis is just a type synonym for @Cursor -> [Cursor]@, it is possible to use-- other standard functions like '>>=' or 'concatMap' similarly.-- -- The operators '&|', '&/', '&//' and '&.//' can be used to combine axes so that the second-- axis works on the context nodes, children, descendants, respectively the context node as -- well as its descendants of the results of the first axis.-- -- The operators '$|', '$/', '$//' and '$.//' can be used to apply an axis (right-hand side)-- to a cursor so that it is applied on the cursor itself, its children, its descendants,-- respectively itself and its descendants.-- -- Note that many of these operators also work on /generalised Axes/ that can return -- lists of something other than Cursors, for example Content elements.typeAxis=Cursor->[Cursor]-- XPath axes as in http://www.w3.org/TR/xpath/#axes-- TODO: Decide whether to use an existing package for this-- | Something that can be used in a predicate check as a boolean.classBooleanawherebool::a->BoolinstanceBooleanBoolwherebool=idinstanceBoolean[a]wherebool=not.nullinstanceBoolean(Maybea)wherebool(Just_)=Truebool_=FalseinstanceBoolean(Eitherab)wherebool(Left_)=Falsebool(Right_)=True-- | A cursor: contains an XML 'Node' and pointers to its children, ancestors and siblings.typeCursor=CG.CursorNode-- | Cut a cursor off from its parent. The idea is to allow restricting the scope of queries on it.cut::Cursor->Cursorcut=fromNode.CG.node-- | Convert a 'Document' to a 'Cursor'. It will point to the document root.fromDocument::Document->CursorfromDocument=fromNode.NodeElement.documentRoot-- | Convert a 'Node' to a 'Cursor' (without parents).fromNode::Node->CursorfromNode=CG.toCursorcswherecs(NodeElement(Element__x))=xcs_=[]-- | Filter cursors that don't pass a check.check::Booleanb=>(Cursor->b)->Axischeckfc=casebool$fcofFalse->[]True->[c]-- | Filter nodes that don't pass a check.checkNode::Booleanb=>(Node->b)->AxischeckNodefc=check(f.node)c-- | Filter elements that don't pass a check, and remove all non-elements.checkElement::Booleanb=>(Element->b)->AxischeckElementfc=casenodecofNodeElemente->casebool$feofTrue->[c]False->[]_->[]-- | Filter elements that don't pass a name check, and remove all non-elements.checkName::Booleanb=>(Name->b)->AxischeckNamefc=checkElement(f.elementName)c-- | Remove all non-elements. Compare roughly to XPath:-- /A node test * is true for any node of the principal node type. For example, child::* will select all element children of the context node [...]/.anyElement::AxisanyElement=checkElement(constTrue)-- | Select only those elements with a matching tag name. XPath:-- /A node test that is a QName is true if and only if the type of the node (see [5 Data Model]) is the principal node type and has an expanded-name equal to the expanded-name specified by the QName./element::Name->Axiselementn=checkName(==n)-- | Select only those elements with a loosely matching tag name. Namespace and case are ignored. XPath:-- /A node test that is a QName is true if and only if the type of the node (see [5 Data Model]) is the principal node type and has an expanded-name equal to the expanded-name specified by the QName./laxElement::T.Text->AxislaxElementn=checkName(on(==)T.toCaseFoldn.nameLocalName)-- | Select only text nodes, and directly give the 'Content' values. XPath:-- /The node test text() is true for any text node./-- -- Note that this is not strictly an 'Axis', but will work with most combinators.content::Cursor->[T.Text]contentc=casenodecof(NodeContentv)->[v]_->[]-- | Select attributes on the current element (or nothing if it is not an element). XPath:-- /the attribute axis contains the attributes of the context node; the axis will be empty unless the context node is an element/-- -- Note that this is not strictly an 'Axis', but will work with most combinators.-- -- The return list of the generalised axis contains as elements lists of 'Content' -- elements, each full list representing an attribute value.attribute::Name->Cursor->[T.Text]attributenc=casenodecofNodeElemente->maybeToList$Map.lookupn$elementAttributese_->[]-- | Select attributes on the current element (or nothing if it is not an element). Namespace and case are ignored. XPath:-- /the attribute axis contains the attributes of the context node; the axis will be empty unless the context node is an element/-- -- Note that this is not strictly an 'Axis', but will work with most combinators.-- -- The return list of the generalised axis contains as elements lists of 'Content' -- elements, each full list representing an attribute value.laxAttribute::T.Text->Cursor->[T.Text]laxAttributenc=casenodecofNodeElemente->do(n',v)<-Map.toList$elementAttributeseguard$(on(==)T.toCaseFold)n(nameLocalNamen')returnv_->[]-- | Select only those element nodes with the given attribute.hasAttribute::Name->AxishasAttributenc=casenodecofNodeElement(Element_as_)->maybe[](const[c])$Map.lookupnas_->[]-- | Select only those element nodes containing the given attribute key/value pair.attributeIs::Name->T.Text->AxisattributeIsnvc=casenodecofNodeElement(Element_as_)->ifJustv==Map.lookupnasthen[c]else[]_->[]force::F.Failureef=>e->[a]->faforcee[]=F.failureeforce_(x:_)=returnxforceM::F.Failureef=>e->[fa]->faforceMe[]=F.failureeforceM_(x:_)=x