def buildReferences(self, ElementTree, allow_duplicate_dfns=False, **kwargs): for dfn in ElementTree.iter(u"dfn"): term = self.getTerm(dfn, **kwargs) if len(term) > 0: if not allow_duplicate_dfns and term in self.dfns: raise DuplicateDfnException(u'The term "%s" is defined more than once' % term) link_to = dfn for parent_element in dfn.iterancestors(tag=etree.Element): if parent_element.tag in utils.heading_content: link_to = parent_element break id = utils.generateID(link_to, **kwargs) link_to.set(u"id", id) self.dfns[term] = id
def buildReferences(self, ElementTree, allow_duplicate_dfns=False, **kwargs): for dfn in ElementTree.iter("dfn"): terms = self.getTerm(dfn, **kwargs).split("|") for term in set(t for t in terms if t): if not allow_duplicate_dfns and term in self.dfns: raise DuplicateDfnException('The term "%s" is defined more than once' % term) link_to = dfn for parent_element in dfn.iterancestors(tag=etree.Element): if parent_element.tag in utils.heading_content: link_to = parent_element break id = utils.generateID(link_to, **kwargs) link_to.set("id", id) self.dfns[term] = id self.instances[term] = []
def buildTerms(self, ElementTree, w3c_compat=False, **kwargs): self.terms.text = "\n" # make a list of all the defining instances of "terms" in the document # -- <dfn> elements dfnList = ElementTree.findall("//dfn") if dfnList: indexNavTop = etree.Element(u"div", { u"class": "index-nav", u"id": "index-terms_top" }) indexNavTop.text = "\n" indexNavTop.tail = "\n" indexNavHelpers = {"top": indexNavTop} self.terms.append(indexNavHelpers["top"]) termFirstLetter = None prevTermFirstLetter = None firstLetters = ["top"] # sort the list of <dfn> terms by the lowercase value of the DOM # textContent of the <dfn> element (concantentation of the <dfn> # text nodes and that of any of its descendant elements) dfnList.sort(key=lambda dfn: utils.textContent(dfn).lower()) for dfn in dfnList: # we don't need the tail, so copy the <dfn> and drop the tail term = deepcopy(dfn) term.tail = None termID = None dfnHasID = False if dfn.get("id"): # if this <dfn> itself has an id, we'll use it as part of the # id on the index entry for this term termID = dfn.get("id") dfnHasID = True elif dfn.getparent().get("id"): # if this <dfn> itself has no id, use the id of its parent # node as the id on the index entry for this term, with or termID = dfn.getparent().get("id") # if we found an id, then create an index entry for this <dfn> # term; otherwise, do nothing further if termID: indexEntry = etree.Element(u"dl") # we want to give this index entry an id attribute based on # the <dfn> or parent of a <dfn> we got the id-attribute # value from earlier; but, if this <dfn> has no id attribute # and has any sibling <dfn>s that also lack id attributes, # we need to further qualify the id attribute here to make # it unique dfnSiblings = int( dfn.xpath("count(preceding-sibling::dfn[not(@id)])")) if not dfnHasID and dfnSiblings > 0: indexEntry = etree.Element(u"dl", { u"id": termID + "_" + str(dfnSiblings) + "_index" }) else: indexEntry = etree.Element(u"dl", {u"id": termID + "_index"}) indexEntry.text = "\n" # termName is container of the name of the term as it appears in the index termName = etree.Element(u"dt") if "id" in term.attrib: del term.attrib["id"] term.tag = "span" term.tail = "\n" termName.append(term) termName.tail = "\n" indexEntry.append(termName) # normalize the text content of each <dfn> in the document # and then normalize the text content of this <dfn>, then # do a case-insensitive comparison of them and count how # many matches we have expr = "count(//dfn\ [normalize-space(translate(.,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'))\ =normalize-space(translate($content,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'))])" if ElementTree.xpath(expr, content=utils.textContent(term)) > 1: # we have more than one <dfn> in the document whose # content is a case-insensitive match for the # textContent of this <dfn>; so, we attempt to # disambiguate them by copying the parent node of the # <dfn> and including that in our output as an excerpt, # to provide the context for the term dfnContext = etree.Element(u"dd", {u"class": u"dfn-excerpt"}) dfnContext.text = "\n" dfnContext.tail = "\n" dfnParentNode = deepcopy(dfn.getparent()) # if length of the parent node isn't greater than 1, # then the <dfn> is the only child node of its parent, # and so there is no useful context we can provide, so # we do nothing. Also, if the parent node is an h1-h6 # heading, we are already listing it in the entry, to # it'd be redundant to be it here too, so we don't if len(dfnParentNode) > 1 and not re.match( "^[hH][1-6]$", dfnParentNode.tag): # we just drop all of the text in this parent up to # the first child element, because it's often just # part of phrase like "The foo attribute" or # something, and we don't need that. But, after we # drop it, we don't want the node to end up starting # with no next at all (because it looks odd in our # output), so we replace it with some characters to # indicate that there's something been ellided if not dfnParentNode[0].tag == "dfn": dfnParentNode.text = "*** " # ...except for the case where we know our current # dfn is the first child element, and then we deal # with handling of that a little further down else: dfnParentNode.text = "" dfnParentNode.tag = "span" # remove ID so that we don't duplicate it if "id" in dfnParentNode.attrib: del dfnParentNode.attrib["id"] descendants = dfnParentNode.xpath( ".//*[self::dfn or @id]") for descendant in descendants: if descendant.tag == "dfn": descendant.tag = "span" if "id" in descendant.attrib: del descendant.attrib["id"] # if the text content of this descendant is the # same as the text content of the term, then we # don't want to repeat it, so instead we # replace it with ellipses if utils.textContent(descendant).lower( ) == utils.textContent(term).lower(): tail = "" if descendant.tail is not None: tail = descendant.tail # drop any children this element might have, # and just put ellipsis in place of it descendant.clear() descendant.text = "..." + tail elif descendant == descendants[0]: # if we get here it means that the first dfn # child of this parent node is _not_ our # current dfn, so we use some alternative # characters (other than ellipses) to # indicate that we've ellided something dfnParentNode.text = "*** " dfnContext.append(dfnParentNode) indexEntry.append(dfnContext) # we need a first letter so that we can build navigational # links for the alphabetic nav bars injected into the index termFirstLetter = utils.textContent(term)[0].upper() if termFirstLetter != prevTermFirstLetter and termFirstLetter.isalpha( ): firstLetters.append(termFirstLetter) indexNavHelpers[termFirstLetter] = etree.Element( u"div", { u"class": "index-nav", u"id": "index-terms_" + termFirstLetter }) prevTermFirstLetter = termFirstLetter self.terms.append(indexNavHelpers[termFirstLetter]) # ######################################################### # make a list of all the instances of terms in the document # that are hyperlinked references back to the <dfn> term # that is the defining instance of this term, as well as # the <dfn> defining instance itself # ######################################################### instanceList = ElementTree.xpath( "//a[substring-after(@href,'#')=$targetID]|//*[@id=$targetID]", targetID=termID) if instanceList: instanceItem = None lastLinkToHeading = None lastInstanceItem = None for instance in instanceList: # each of these term instances is an <a> hyperlink # without an id attribute, but we need each to have # an id attribute so that we can link back to it # from the index of terms; so, create an id for each instanceID = utils.generateID(instance, **kwargs) instance.set(u"id", instanceID) # make a link that's a copy of the node of the h1-h6 # heading for the section that contains this # instance hyperlink linkToHeading = self.getAncestorHeadingLink( instance, instanceID) if not instance.tag == u"a": linkToHeading.set(u"class", "dfn-ref") # if this heading is not the same as one that we've # already added to the index entry for this term, # then process the heading if lastLinkToHeading is None or \ utils.textContent(linkToHeading) != utils.textContent(lastLinkToHeading): instanceItem = etree.Element(u"dd") instanceItem.text = "\n" lastLinkToHeading = linkToHeading n = 1 # we wait to add the item for the previous # instance at this point because we need to # delay adding in order to see if for this # instance there are multiple references to the # same ancestor heading (if there are, we append # link numbers to them, instead of repeating the # heading; see below) if lastInstanceItem is not None: #print(etree.tostring(lastInstanceItem,method="text")) indexEntry.append(lastInstanceItem) lastInstanceItem = instanceItem linkToHeading.tail = "\n" instanceItem.append(linkToHeading) instanceItem.tail = "\n" # otherwise, this heading is the same as one that # we've already added to the index entry for this # term; so instead of reprocessing the heading, we # just append one or more link numbers to it else: n += 1 counterLink = etree.Element( u"a", { u"href": "#" + instanceID, u"class": "index-counter" }) if not instance.tag == u"a": counterLink.set(u"class", "dfn-ref") else: counterLink.set(u"class", "index-counter") counterLink.text = "(" + str(n) + ")" counterLink.tail = "\n" instanceItem.append(counterLink) # if the value of our n counter is still at 1 at # this point, it means the document contains only # one instance of a reference this term, so we need # to add that instance now if n == 1: indexEntry.append(instanceItem) if not len(instanceList) > 1: # if we don't have more than one item in this list, it # means the <dfn> defining instance is the only item in # the list, and the document contains no hyperlinked # references back to that defining instance at all, so # we need to set a flag to indicate that indexEntry.set(u"class", "has-norefs") self.terms.append(indexEntry) indexEntry.tail = "\n" # ###################################################################### # inject some alphabetic nav hyperlink bars into the index, strictly # for convenience purposes # ###################################################################### navLetters = etree.Element(u"p") navLetters.text = "\n" navLetters.tail = "\n" navLettersClones = {} # reverse the letters list so that we can just pop off it firstLetters.append("end") firstLetters.reverse() while (firstLetters): letter = firstLetters.pop() navLetter = etree.Element(u"a", {u"href": "#index-terms_" + letter}) navLetter.text = letter navLetter.tail = "\n" navLetters.append(navLetter) for key, navNode in indexNavHelpers.items(): # this seems really hacky... but we need some way to manage multiple # copies of the sets of nav hyperlink letters we inject into the # index; otherwise, how to do it without just moving a single node # around instead of copying it... navLettersClones[key] = deepcopy(navLetters) navNode.text = "\n" navNode.append(navLettersClones[key]) navNode.tail = "\n" navLettersEnd = deepcopy(navLetters) indexNavEnd = etree.Element(u"div", { u"class": "index-nav", u"id": "index-terms_end" }) indexNavEnd.text = "\n" indexNavEnd.tail = "\n" indexNavEnd.append(navLettersEnd) indexNavHelpers = {"end": indexNavEnd} self.terms.append(indexNavHelpers["end"]) self.terms.tail = "\n"
def buildToc(self, ElementTree, min_depth=2, max_depth=6, w3c_compat=False, w3c_compat_class_toc=False, **kwargs): # Build the outline of the document outline_creator = outliner.Outliner(ElementTree, **kwargs) outline = outline_creator.build(**kwargs) # Get a list of all the top level sections, and their depth (0) sections = [(section, 0) for section in reversed(outline)] # Numbering num = [] # Loop over all sections in a DFS while sections: # Get the section and depth at the end of list section, depth = sections.pop() # If we have a header, regardless of how deep we are if section.header is not None: # Get the element that represents the section header's text if section.header.tag == u"hgroup": i = 1 while i <= 6: header_text = section.header.find(u".//h" + unicode(i)) if header_text is not None: break i += 1 else: header_text = None else: header_text = section.header else: header_text = None # If we have a section heading text element, regardless of depth if header_text is not None: # Remove any existing number for element in header_text.findall(u".//span"): if utils.elementHasClass(element, u"secno"): # Copy content, to prepare for the node being # removed utils.copyContentForRemoval(element, text=False, children=False) # Remove the element (we can do this as we're not # iterating over the elements, but over a list) element.getparent().remove(element) # Check we're in the valid depth range (min/max_depth are 1 based, # depth is 0 based) if depth >= min_depth - 1 and depth <= max_depth - 1: # Calculate the corrected depth (i.e., the actual depth within # the numbering/TOC) corrected_depth = depth - min_depth + 1 # Numbering: # No children, no sibling, move back to parent's sibling if corrected_depth + 1 < len(num): del num[corrected_depth + 1:] # Children elif corrected_depth == len(num): num.append(0) # Increment the current section's number if header_text is not None and \ not utils.elementHasClass(header_text, u"no-num") or \ header_text is None and section: num[-1] += 1 # Get the current TOC section for this depth, and add another # item to it if header_text is not None and \ not utils.elementHasClass(header_text, u"no-toc") or \ header_text is None and section: # Find the appropriate section of the TOC i = 0 toc_section = self.toc while i < corrected_depth: try: # If the final li has no children, or the last # children isn't an ol element if len(toc_section[-1]) == 0 or \ toc_section[-1][-1].tag != u"ol": toc_section[-1].append(etree.Element(u"ol")) self.indentNode(toc_section[-1][-1], (i + 1) * 2, **kwargs) if w3c_compat or w3c_compat_class_toc: toc_section[-1][-1].set(u"class", u"toc") except IndexError: # If the current ol has no li in it toc_section.append(etree.Element(u"li")) self.indentNode(toc_section[0], (i + 1) * 2 - 1, **kwargs) toc_section[0].append(etree.Element(u"ol")) self.indentNode(toc_section[0][0], (i + 1) * 2, **kwargs) if w3c_compat or w3c_compat_class_toc: toc_section[0][0].set(u"class", u"toc") # TOC Section is now the final child (ol) of the final # item (li) in the previous section assert toc_section[-1].tag == u"li" assert toc_section[-1][-1].tag == u"ol" toc_section = toc_section[-1][-1] i += 1 # Add the current item to the TOC item = etree.Element(u"li") toc_section.append(item) self.indentNode(item, (i + 1) * 2 - 1, **kwargs) # If we have a header if header_text is not None: # Add ID to header id = utils.generateID(header_text, **kwargs) if header_text.get(u"id") is not None: del header_text.attrib[u"id"] section.header.set(u"id", id) # Add number, if @class doesn't contain no-num if not utils.elementHasClass(header_text, u"no-num"): header_text[0:0] = [etree.Element(u"span", {u"class": u"secno"})] header_text[0].tail = header_text.text header_text.text = None header_text[0].text = u".".join(map(unicode, num)) header_text[0].text += u" " # Add to TOC, if @class doesn't contain no-toc if not utils.elementHasClass(header_text, u"no-toc"): link = deepcopy(header_text) item.append(link) # Make it link to the header link.tag = u"a" link.set(u"href", u"#" + id) # Remove interactive content child elements utils.removeInteractiveContentChildren(link) # Remove other child elements for element_name in remove_elements_from_toc: # Iterate over all the desendants of the new link # with that element name for element in link.findall(u".//" + element_name): # Copy content, to prepare for the node being # removed utils.copyContentForRemoval(element) # Remove the element (we can do this as we're # not iterating over the elements, but over a # list) element.getparent().remove(element) # Remove unwanted attributes for element in link.iter(tag=etree.Element): for attribute_name in remove_attributes_from_toc: if element.get(attribute_name) is not None: del element.attrib[attribute_name] # We don't want the old tail link.tail = None # Check we haven't changed the content in all of that assert utils.textContent(header_text) == \ utils.textContent(link) # Add subsections in reverse order (so the next one is executed # next) with a higher depth value sections.extend([(child_section, depth + 1) for child_section in reversed(section)])
def buildTerms(self, ElementTree, w3c_compat=False, **kwargs): self.terms.text = "\n" # make a list of all the defining instances of "terms" in the document # -- <dfn> elements dfnList = ElementTree.findall("//dfn") if dfnList: indexNavTop = etree.Element(u"div",{u"class": "index-nav", u"id": "index-terms_top"}) indexNavTop.text = "\n" indexNavTop.tail = "\n" indexNavHelpers = {"top": indexNavTop} self.terms.append(indexNavHelpers["top"]) termFirstLetter = None prevTermFirstLetter = None firstLetters = ["top"] # sort the list of <dfn> terms by the lowercase value of the DOM # textContent of the <dfn> element (concantentation of the <dfn> # text nodes and that of any of its descendant elements) dfnList.sort(key=lambda dfn: dfn.text_content().lower()) for dfn in dfnList: # we don't need the tail, so copy the <dfn> and drop the tail term = deepcopy(dfn) term.tail = None termID = None dfnHasID = False if dfn.get("id"): # if this <dfn> itself has an id, we'll use it as part of the # id on the index entry for this term termID = dfn.get("id") dfnHasID = True elif dfn.getparent().get("id"): # if this <dfn> itself has no id, use the id of its parent # node as the id on the index entry for this term, with or termID = dfn.getparent().get("id") # if we found an id, then create an index entry for this <dfn> # term; otherwise, do nothing further if termID: indexEntry = etree.Element(u"dl") # we want to give this index entry an id attribute based on # the <dfn> or parent of a <dfn> we got the id-attribute # value from earlier; but, if this <dfn> has no id attribute # and has any sibling <dfn>s that also lack id attributes, # we need to further qualify the id attribute here to make # it unique dfnSiblings = int(dfn.xpath("count(preceding-sibling::dfn[not(@id)])")) if not dfnHasID and dfnSiblings > 0: indexEntry = etree.Element(u"dl",{u"id": termID+"_"+str(dfnSiblings)+"_index"}) else: indexEntry = etree.Element(u"dl",{u"id": termID+"_index"}) indexEntry.text = "\n" # termName is container of the name of the term as it appears in the index termName = etree.Element(u"dt") if "id" in term.attrib: del term.attrib["id"] term.tag = "span" term.tail = "\n" termName.append(term); termName.tail= "\n" indexEntry.append(termName) # normalize the text content of each <dfn> in the document # and then normalize the text content of this <dfn>, then # do a case-insensitive comparison of them and count how # many matches we have expr = "count(//dfn\ [normalize-space(translate(.,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'))\ =normalize-space(translate($content,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'))])" if ElementTree.xpath(expr, content = term.text_content()) > 1: # we have more than one <dfn> in the document whose # content is a case-insensitive match for the # textContent of this <dfn>; so, we attempt to # disambiguate them by copying the parent node of the # <dfn> and including that in our output as an excerpt, # to provide the context for the term dfnContext = etree.Element(u"dd",{u"class": u"dfn-excerpt"}) dfnContext.text = "\n" dfnContext.tail = "\n" dfnParentNode = deepcopy(dfn.getparent()) # if length of the parent node isn't greater than 1, # then the <dfn> is the only child node of its parent, # and so there is no useful context we can provide, so # we do nothing. Also, if the parent node is an h1-h6 # heading, we are already listing it in the entry, to # it'd be redundant to be it here too, so we don't if len(dfnParentNode) > 1 and not re.match("^[hH][1-6]$",dfnParentNode.tag): # we just drop all of the text in this parent up to # the first child element, because it's often just # part of phrase like "The foo attribute" or # something, and we don't need that. But, after we # drop it, we don't want the node to end up starting # with no next at all (because it looks odd in our # output), so we replace it with some characters to # indicate that there's something been ellided if not dfnParentNode[0].tag == "dfn": dfnParentNode.text = "*** " # ...except for the case where we know our current # dfn is the first child element, and then we deal # with handling of that a little further down else: dfnParentNode.text = "" dfnParentNode.tag = "span" # remove ID so that we don't duplicate it if "id" in dfnParentNode.attrib: del dfnParentNode.attrib["id"] descendants = dfnParentNode.xpath(".//*[self::dfn or @id]") for descendant in descendants: if descendant.tag == "dfn": descendant.tag = "span" if "id" in descendant.attrib: del descendant.attrib["id"] # if the text content of this descendant is the # same as the text content of the term, then we # don't want to repeat it, so instead we # replace it with ellipses if descendant.text_content().lower() == term.text_content().lower(): tail = "" if descendant.tail is not None: tail = descendant.tail # drop any children this element might have, # and just put ellipsis in place of it descendant.clear() descendant.text = "..."+tail elif descendant == descendants[0]: # if we get here it means that the first dfn # child of this parent node is _not_ our # current dfn, so we use some alternative # characters (other than ellipses) to # indicate that we've ellided something dfnParentNode.text = "*** " dfnContext.append(dfnParentNode) indexEntry.append(dfnContext) # we need a first letter so that we can build navigational # links for the alphabetic nav bars injected into the index termFirstLetter = term.text_content()[0].upper() if termFirstLetter != prevTermFirstLetter and termFirstLetter.isalpha(): firstLetters.append(termFirstLetter) indexNavHelpers[termFirstLetter] = etree.Element(u"div",{u"class": "index-nav", u"id": "index-terms_"+termFirstLetter}) prevTermFirstLetter = termFirstLetter self.terms.append(indexNavHelpers[termFirstLetter]) # ######################################################### # make a list of all the instances of terms in the document # that are hyperlinked references back to the <dfn> term # that is the defining instance of this term, as well as # the <dfn> defining instance itself # ######################################################### instanceList = ElementTree.xpath("//a[substring-after(@href,'#')=$targetID]|//*[@id=$targetID]", targetID = termID) if instanceList: instanceItem = None lastLinkToHeading = None lastInstanceItem = None for instance in instanceList: # each of these term instances is an <a> hyperlink # without an id attribute, but we need each to have # an id attribute so that we can link back to it # from the index of terms; so, create an id for each instanceID = utils.generateID(instance, **kwargs) instance.set(u"id",instanceID) # make a link that's a copy of the node of the h1-h6 # heading for the section that contains this # instance hyperlink linkToHeading = self.getAncestorHeadingLink(instance, instanceID) if not instance.tag == u"a": linkToHeading.set(u"class","dfn-ref") # if this heading is not the same as one that we've # already added to the index entry for this term, # then process the heading if lastLinkToHeading is None or linkToHeading.text_content() != lastLinkToHeading.text_content(): instanceItem = etree.Element(u"dd") instanceItem.text = "\n" lastLinkToHeading = linkToHeading n = 1 # we wait to add the item for the previous # instance at this point because we need to # delay adding in order to see if for this # instance there are multiple references to the # same ancestor heading (if there are, we append # link numbers to them, instead of repeating the # heading; see below) if lastInstanceItem is not None: #print(etree.tostring(lastInstanceItem,method="text")) indexEntry.append(lastInstanceItem) lastInstanceItem = instanceItem linkToHeading.tail = "\n" instanceItem.append(linkToHeading) instanceItem.tail = "\n" # otherwise, this heading is the same as one that # we've already added to the index entry for this # term; so instead of reprocessing the heading, we # just append one or more link numbers to it else: n += 1 counterLink = etree.Element(u"a",{u"href": "#"+instanceID, u"class": "index-counter"}) if not instance.tag == u"a": counterLink.set(u"class","dfn-ref") else: counterLink.set(u"class","index-counter") counterLink.text = "("+str(n)+")" counterLink.tail = "\n" instanceItem.append(counterLink) # if the value of our n counter is still at 1 at # this point, it means the document contains only # one instance of a reference this term, so we need # to add that instance now if n == 1: indexEntry.append(instanceItem) if not len(instanceList) > 1: # if we don't have more than one item in this list, it # means the <dfn> defining instance is the only item in # the list, and the document contains no hyperlinked # references back to that defining instance at all, so # we need to set a flag to indicate that indexEntry.set(u"class","has-norefs") self.terms.append(indexEntry) indexEntry.tail = "\n" # ###################################################################### # inject some alphabetic nav hyperlink bars into the index, strictly # for convenience purposes # ###################################################################### navLetters = etree.Element(u"p") navLetters.text = "\n" navLetters.tail = "\n" navLettersClones = {} # reverse the letters list so that we can just pop off it firstLetters.append("end") firstLetters.reverse() while(firstLetters): letter = firstLetters.pop() navLetter = etree.Element(u"a",{u"href": "#index-terms_"+letter}) navLetter.text = letter navLetter.tail = "\n" navLetters.append(navLetter) for key, navNode in indexNavHelpers.items(): # this seems really hacky... but we need some way to manage multiple # copies of the sets of nav hyperlink letters we inject into the # index; otherwise, how to do it without just moving a single node # around instead of copying it... navLettersClones[key] = deepcopy(navLetters) navNode.text = "\n" navNode.append(navLettersClones[key]) navNode.tail = "\n" navLettersEnd = deepcopy(navLetters) indexNavEnd = etree.Element(u"div",{u"class": "index-nav", u"id": "index-terms_end"}) indexNavEnd.text = "\n" indexNavEnd.tail = "\n" indexNavEnd.append(navLettersEnd) indexNavHelpers = {"end": indexNavEnd} self.terms.append(indexNavHelpers["end"]) self.terms.tail = "\n"