예제 #1
0
파일: __init__.py 프로젝트: nromey/nvda
 def _getFormatFieldAtRange(self, range, formatConfig):
     formatField = textInfos.FormatField()
     if formatConfig["reportFontName"]:
         try:
             fontNameValue = range.GetAttributeValue(
                 UIAHandler.UIA_FontNameAttributeId)
         except COMError:
             fontNameValue = UIAHandler.handler.reservedNotSupportedValue
         if fontNameValue != UIAHandler.handler.reservedNotSupportedValue:
             formatField["font-name"] = fontNameValue
     if formatConfig["reportFontSize"]:
         try:
             fontSizeValue = range.GetAttributeValue(
                 UIAHandler.UIA_FontSizeAttributeId)
         except COMError:
             fontSizeValue = UIAHandler.handler.reservedNotSupportedValue
         if fontSizeValue != UIAHandler.handler.reservedNotSupportedValue:
             formatField['font-size'] = "%g pt" % float(fontSizeValue)
     if formatConfig["reportHeadings"]:
         try:
             styleIDValue = range.GetAttributeValue(
                 UIAHandler.UIA_StyleIdAttributeId)
         except COMError:
             styleIDValue = UIAHandler.handler.reservedNotSupportedValue
         if UIAHandler.StyleId_Heading1 <= styleIDValue <= UIAHandler.StyleId_Heading9:
             formatField["heading-level"] = (
                 styleIDValue - UIAHandler.StyleId_Heading1) + 1
     return textInfos.FieldCommand("formatChange", formatField)
예제 #2
0
    def _startElementHandler(self, tagName, attrs):
        if tagName == 'unich':
            data = attrs.get('value', None)
            if data is not None:
                try:
                    data = chr(int(data))
                except ValueError:
                    data = textUtils.REPLACEMENT_CHAR
                self._CharacterDataHandler(
                    data, processBufferedSurrogates=isLowSurrogate(data))
            return
        elif tagName == 'control':
            newAttrs = textInfos.ControlField(attrs)
            self._commandList.append(
                textInfos.FieldCommand("controlStart", newAttrs))
        elif tagName == 'text':
            newAttrs = textInfos.FormatField(attrs)
            self._commandList.append(
                textInfos.FieldCommand("formatChange", newAttrs))
        else:
            raise ValueError("Unknown tag name: %s" % tagName)

        # Normalise attributes common to both field types.
        try:
            newAttrs["_startOfNode"] = newAttrs["_startOfNode"] == "1"
        except KeyError:
            pass
        try:
            newAttrs["_endOfNode"] = newAttrs["_endOfNode"] == "1"
        except KeyError:
            pass
def getFormatField(textInfo, formatConfig):
	formatField = textInfos.FormatField()
	for field in textInfo.getTextWithFields(formatConfig):
		if isinstance(field, textInfos.FieldCommand) and\
			isinstance(field.field, textInfos.FormatField):
			formatField.update(field.field)
	return formatField
예제 #4
0
 def _getFormatFieldAndOffsets(self,
                               offset,
                               formatConfig,
                               calculateOffsets=True):
     formatField = textInfos.FormatField()
     fontObj = self.obj.excelCellObject.font
     if formatConfig['reportFontName']:
         formatField['font-name'] = fontObj.name
     if formatConfig['reportFontSize']:
         formatField['font-size'] = str(fontObj.size)
     if formatConfig['reportFontAttributes']:
         formatField['bold'] = fontObj.bold
         formatField['italic'] = fontObj.italic
         formatField['underline'] = fontObj.underline
     if formatConfig['reportColor']:
         try:
             formatField['color'] = colors.RGB.fromCOLORREF(
                 int(fontObj.color))
         except COMError:
             pass
         try:
             formatField['background-color'] = colors.RGB.fromCOLORREF(
                 int(self.obj.excelCellObject.interior.color))
         except COMError:
             pass
     return formatField, (self._startOffset, self._endOffset)
예제 #5
0
 def getParagraphStyle(self, info):
     formatField = textInfos.FormatField()
     formatConfig = config.conf['documentFormatting']
     for field in info.getTextWithFields(formatConfig):
         #if isinstance(field,textInfos.FieldCommand): and isinstance(field.field,textInfos.FormatField):
         try:
             formatField.update(field.field)
         except:
             pass
     result = [
         formatField.get(fieldName, None) for fieldName in self.styleFields
     ]
     return tuple(result)
 def getTextWithFields(self, formatConfig=None):
     commands = []
     if self.isCollapsed:
         return commands
     if not formatConfig:
         formatConfig = config.conf["documentFormatting"]
     left, top = self._consoleCoordFromOffset(self._startOffset)
     right, bottom = self._consoleCoordFromOffset(self._endOffset - 1)
     rect = wincon.SMALL_RECT(left, top, right, bottom)
     if bottom - top > 0:  #offsets span multiple lines
         rect.Left = 0
         rect.Right = self.consoleScreenBufferInfo.dwSize.x - 1
         length = self.consoleScreenBufferInfo.dwSize.x * (bottom - top + 1)
     else:
         length = self._endOffset - self._startOffset
     buf = wincon.ReadConsoleOutput(consoleOutputHandle, length, rect)
     if bottom - top > 0:
         buf = buf[left:len(buf) -
                   (self.consoleScreenBufferInfo.dwSize.x - right) + 1]
     lastAttr = None
     lastText = []
     boundEnd = self._startOffset
     for i, c in enumerate(buf):
         if self._startOffset + i == boundEnd:
             field, (boundStart, boundEnd) = self._getFormatFieldAndOffsets(
                 boundEnd, formatConfig)
             if lastText:
                 commands.append("".join(lastText))
                 lastText = []
             commands.append(textInfos.FieldCommand("formatChange", field))
         if not c.Attributes == lastAttr:
             formatField = textInfos.FormatField()
             if formatConfig['reportColor']:
                 formatField["color"] = CONSOLE_COLORS_TO_RGB[c.Attributes
                                                              & 0x0f]
                 formatField["background-color"] = CONSOLE_COLORS_TO_RGB[
                     (c.Attributes >> 4) & 0x0f]
             if formatConfig[
                     'reportFontAttributes'] and c.Attributes & COMMON_LVB_UNDERSCORE:
                 formatField['underline'] = True
             if formatField:
                 if lastText:
                     commands.append("".join(lastText))
                     lastText = []
                 command = textInfos.FieldCommand("formatChange",
                                                  formatField)
                 commands.append(command)
             lastAttr = c.Attributes
         lastText.append(c.Char)
     commands.append("".join(lastText))
     return commands
예제 #7
0
	def _getFormatFieldAndOffsets(self,offset,formatConfig,calculateOffsets=True):
		"""Retrieve the formatting information for a given offset and the offsets spanned by that field.
		Subclasses must override this if support for text formatting is desired.
		The base implementation associates text with line numbers if possible.
		"""
		formatField=textInfos.FormatField()
		startOffset,endOffset=self._startOffset,self._endOffset
		if formatConfig["reportLineNumber"]:
			if calculateOffsets:
				startOffset,endOffset=self._getLineOffsets(offset)
			lineNum=self._getLineNumFromOffset(offset)
			if lineNum is not None:
				formatField["line-number"]=lineNum+1
		return formatField,(startOffset,endOffset)
예제 #8
0
	def reportFocus(self):
		# #4878: Excel specific code for speaking format changes on the focused object.
		info=self.makeTextInfo(textInfos.POSITION_FIRST)
		info.expand(textInfos.UNIT_CHARACTER)
		formatField=textInfos.FormatField()
		formatConfig=config.conf['documentFormatting']
		for field in info.getTextWithFields(formatConfig):
			if isinstance(field,textInfos.FieldCommand) and isinstance(field.field,textInfos.FormatField):
				formatField.update(field.field)
		if not hasattr(self.parent,'_formatFieldSpeechCache'):
			self.parent._formatFieldSpeechCache={}
		text=speech.getFormatFieldSpeech(formatField,attrsCache=self.parent._formatFieldSpeechCache,formatConfig=formatConfig) if formatField else None
		if text:
			speech.speakText(text)
		super(ExcelCell,self).reportFocus()
예제 #9
0
파일: __init__.py 프로젝트: nromey/nvda
 def _getFormatFieldsAndText(self, tempRange, formatConfig):
     if not self.allowGetFormatFieldsAndTextOnDegenerateUIARanges and tempRange.compareEndpoints(
             UIAHandler.TextPatternRangeEndpoint_Start, tempRange,
             UIAHandler.TextPatternRangeEndpoint_End) == 0:
         return
     formatField = self._getFormatFieldAtRange(tempRange, formatConfig)
     if formatConfig["reportSpellingErrors"]:
         try:
             annotationTypes = tempRange.GetAttributeValue(
                 UIAHandler.UIA_AnnotationTypesAttributeId)
         except COMError:
             annotationTypes = UIAHandler.handler.reservedNotSupportedValue
         if annotationTypes == UIAHandler.AnnotationType_SpellingError:
             formatField.field["invalid-spelling"] = True
             yield formatField
             yield tempRange.GetText(-1)
         elif annotationTypes == UIAHandler.handler.ReservedMixedAttributeValue:
             for r in self._iterUIARangeByUnit(tempRange,
                                               UIAHandler.TextUnit_Word):
                 text = r.GetText(-1)
                 if not text:
                     continue
                 r.MoveEndpointByRange(
                     UIAHandler.TextPatternRangeEndpoint_End, r,
                     UIAHandler.TextPatternRangeEndpoint_Start)
                 r.ExpandToEnclosingUnit(UIAHandler.TextUnit_Character)
                 try:
                     annotationTypes = r.GetAttributeValue(
                         UIAHandler.UIA_AnnotationTypesAttributeId)
                 except COMError:
                     annotationTypes = UIAHandler.handler.reservedNotSupportedValue
                 newField = textInfos.FormatField()
                 newField.update(formatField.field)
                 if annotationTypes == UIAHandler.AnnotationType_SpellingError:
                     newField["invalid-spelling"] = True
                 yield textInfos.FieldCommand("formatChange", newField)
                 yield text
         else:
             yield formatField
             yield tempRange.GetText(-1)
     else:
         yield formatField
         yield tempRange.GetText(-1)
예제 #10
0
	def _hasBackground(self,colors,ti=None) :
		cfg = {
			"detectFormatAfterCursor":False,
			"reportFontName":False,"reportFontSize":False,"reportFontAttributes":False,"reportColor":True,"reportRevisions":False,
			"reportStyle":False,"reportAlignment":False,"reportSpellingErrors":False,
			"reportPage":False,"reportLineNumber":False,"reportTables":False,
			"reportLinks":False,"reportHeadings":False,"reportLists":False,
			"reportBlockQuotes":False,"reportComments":False,
		}
		retval = dict((color,False) for color in colors)
		if not ti :
			ti = self.makeTextInfo(textInfos.POSITION_SELECTION)
			ti._endOffset = ti._startOffset
			ti.collapse()
			ti.expand(textInfos.UNIT_CHARACTER)
		formatField=textInfos.FormatField()
		for field in ti.getTextWithFields(cfg):
			if isinstance(field,textInfos.FieldCommand) and isinstance(field.field,textInfos.FormatField):
				if 'background-color' in field.field :
					formatField.update(field.field)
					rgb = formatField['background-color']
					if rgb in retval :
						retval[rgb] = True
		return retval
예제 #11
0
	def _getFormatFieldAndOffsets(self,offset,formatConfig,calculateOffsets=True):
		formatField=textInfos.FormatField()
		fontObj=self.obj.excelCellObject.font
		if formatConfig['reportAlignment']:
			value=alignmentLabels.get(self.obj.excelCellObject.horizontalAlignment)
			if value:
				formatField['text-align']=value
			value=alignmentLabels.get(self.obj.excelCellObject.verticalAlignment)
			if value:
				formatField['vertical-align']=value
		if formatConfig['reportFontName']:
			formatField['font-name']=fontObj.name
		if formatConfig['reportFontSize']:
			formatField['font-size']=str(fontObj.size)
		if formatConfig['reportFontAttributes']:
			formatField['bold']=fontObj.bold
			formatField['italic']=fontObj.italic
			underline=fontObj.underline
			formatField['underline']=False if underline is None or underline==xlUnderlineStyleNone else True
		if formatConfig['reportStyle']:
			try:
				styleName=self.obj.excelCellObject.style.nameLocal
			except COMError:
				styleName=None
			if styleName:
				formatField['style']=styleName
		if formatConfig['reportColor']:
			try:
				formatField['color']=colors.RGB.fromCOLORREF(int(fontObj.color))
			except COMError:
				pass
			try:
				formatField['background-color']=colors.RGB.fromCOLORREF(int(self.obj.excelCellObject.interior.color))
			except COMError:
				pass
		return formatField,(self._startOffset,self._endOffset)
예제 #12
0
파일: soffice.py 프로젝트: wafiqtaher/nvda
	def _getFormatFieldAndOffsets(self,offset,formatConfig,calculateOffsets=True):
		obj = self.obj
		try:
			startOffset,endOffset,attribsString=obj.IAccessibleTextObject.attributes(offset)
		except COMError:
			log.debugWarning("could not get attributes",exc_info=True)
			return textInfos.FormatField(),(self._startOffset,self._endOffset)
		formatField=textInfos.FormatField()
		if not attribsString and offset>0:
			try:
				attribsString=obj.IAccessibleTextObject.attributes(offset-1)[2]
			except COMError:
				pass
		if attribsString:
			formatField.update(IAccessibleHandler.splitIA2Attribs(attribsString))

		try:
			escapement = int(formatField["CharEscapement"])
			if escapement < 0:
				textPos = "sub"
			elif escapement > 0:
				textPos = "super"
			else:
				textPos = "baseline"
			formatField["text-position"] = textPos
		except KeyError:
			pass
		try:
			formatField["font-name"] = formatField["CharFontName"]
		except KeyError:
			pass
		try:
			formatField["font-size"] = "%spt" % formatField["CharHeight"]
		except KeyError:
			pass
		try:
			formatField["italic"] = formatField["CharPosture"] == "2"
		except KeyError:
			pass
		try:
			formatField["strikethrough"] = formatField["CharStrikeout"] == "1"
		except KeyError:
			pass
		try:
			underline = formatField["CharUnderline"]
			if underline == "10":
				# Symphony doesn't provide for semantic communication of spelling errors, so we have to rely on the WAVE underline type.
				formatField["invalid-spelling"] = True
			else:
				formatField["underline"] = underline != "0"
		except KeyError:
			pass
		try:
			formatField["bold"] = float(formatField["CharWeight"]) > 100
		except KeyError:
			pass
		try:
			color=formatField.pop('CharColor')
		except KeyError:
			color=None
		if color:
			formatField['color']=colors.RGB.fromString(color) 
		try:
			backgroundColor=formatField.pop('CharBackColor')
		except KeyError:
			backgroundColor=None
		if backgroundColor:
			formatField['background-color']=colors.RGB.fromString(backgroundColor)

		# optimisation: Assume a hyperlink occupies a full attribute run.
		try:
			if obj.IAccessibleTextObject.QueryInterface(IAccessibleHandler.IAccessibleHypertext).hyperlinkIndex(offset) != -1:
				formatField["link"] = True
		except COMError:
			pass

		if offset == 0:
			# Only include the list item prefix on the first line of the paragraph.
			numbering = formatField.get("Numbering")
			if numbering:
				formatField["line-prefix"] = numbering.get("NumberingPrefix") or numbering.get("BulletChar")

		if obj.hasFocus:
			# Symphony exposes some information for the caret position as attributes on the document object.
			# optimisation: Use the tree interceptor to get the document.
			try:
				docAttribs = obj.treeInterceptor.rootNVDAObject.IA2Attributes
			except AttributeError:
				# No tree interceptor, so we can't efficiently fetch this info.
				pass
			else:
				try:
					formatField["page-number"] = docAttribs["page-number"]
				except KeyError:
					pass
				try:
					formatField["line-number"] = docAttribs["line-number"]
				except KeyError:
					pass

		return formatField,(startOffset,endOffset)
예제 #13
0
 def getTextWithFields(self, formatConfig=None):
     if self.isCollapsed:
         # #7652: We cannot fetch fields on collapsed ranges otherwise we end up with repeating controlFields in braille (such as list list list).
         return []
     fields = super(WordDocumentTextInfo,
                    self).getTextWithFields(formatConfig=formatConfig)
     if len(fields) == 0:
         # Nothing to do... was probably a collapsed range.
         return fields
     # Sometimes embedded objects and graphics In MS Word can cause a controlStart then a controlEnd with no actual formatChange / text in the middle.
     # SpeakTextInfo always expects that the first lot of controlStarts will always contain some text.
     # Therefore ensure that the first lot of controlStarts does contain some text by inserting a blank formatChange and empty string in this case.
     for index in range(len(fields)):
         field = fields[index]
         if isinstance(field, textInfos.FieldCommand
                       ) and field.command == "controlStart":
             continue
         elif isinstance(
                 field,
                 textInfos.FieldCommand) and field.command == "controlEnd":
             formatChange = textInfos.FieldCommand("formatChange",
                                                   textInfos.FormatField())
             fields.insert(index, formatChange)
             fields.insert(index + 1, "")
         break
     ##7971: Microsoft Word exposes list bullets as part of the actual text.
     # This then confuses NVDA's braille cursor routing as it expects that there is a one-to-one mapping between characters in the text string and   unit character moves.
     # Therefore, detect when at the start of a list, and strip the bullet from the text string, placing it in the text's formatField as line-prefix.
     listItemStarted = False
     lastFormatField = None
     for index in range(len(fields)):
         field = fields[index]
         if isinstance(field, textInfos.FieldCommand
                       ) and field.command == "controlStart":
             if field.field.get(
                     'role'
             ) == controlTypes.ROLE_LISTITEM and field.field.get(
                     '_startOfNode'):
                 # We are in the start of a list item.
                 listItemStarted = True
         elif isinstance(field, textInfos.FieldCommand
                         ) and field.command == "formatChange":
             # This is the most recent formatField we have seen.
             lastFormatField = field.field
         elif listItemStarted and isinstance(field, str):
             # This is the first text string within the list.
             # Remove the text up to the first space, and store it as line-prefix which NVDA will appropriately speak/braille as a bullet.
             try:
                 spaceIndex = field.index(' ')
             except ValueError:
                 log.debugWarning("No space found in this text string")
                 break
             prefix = field[0:spaceIndex]
             fields[index] = field[spaceIndex + 1:]
             lastFormatField['line-prefix'] = prefix
             # Let speech know that line-prefix is safe to be spoken always, as it will only be exposed on the very first formatField on the list item.
             lastFormatField['line-prefix_speakAlways'] = True
             break
         else:
             # Not a controlStart, formatChange or text string. Nothing to do.
             break
     # Fill in page number attributes where NVDA expects
     try:
         page = fields[0].field['page-number']
     except KeyError:
         page = None
     if page is not None:
         for field in fields:
             if isinstance(field, textInfos.FieldCommand) and isinstance(
                     field.field, textInfos.FormatField):
                 field.field['page-number'] = page
     # MS Word can sometimes return a higher ancestor in its textRange's children.
     # E.g. a table inside a table header.
     # This does not cause a loop, but does cause information to be doubled
     # Detect these duplicates and remove them from the generated fields.
     seenStarts = set()
     pendingRemoves = []
     index = 0
     for index, field in enumerate(fields):
         if isinstance(field, textInfos.FieldCommand
                       ) and field.command == "controlStart":
             runtimeID = field.field['runtimeID']
             if not runtimeID:
                 continue
             if runtimeID in seenStarts:
                 pendingRemoves.append(field.field)
             else:
                 seenStarts.add(runtimeID)
         elif seenStarts:
             seenStarts.clear()
     index = 0
     while index < len(fields):
         field = fields[index]
         if isinstance(field, textInfos.FieldCommand) and any(
                 x is field.field for x in pendingRemoves):
             del fields[index]
         else:
             index += 1
     return fields
예제 #14
0
    def getTextWithFields(  # noqa: C901
        self,
        formatConfig: Optional[Dict] = None
    ) -> textInfos.TextInfo.TextWithFieldsT:
        fields = None
        # #11043: when a non-collapsed text range is positioned within a blank table cell
        # MS Word does not return the table  cell as an enclosing element,
        # Thus NVDa thinks the range is not inside the cell.
        # This can be detected by asking for the first 2 characters of the range's text,
        # Which will either be an empty string, or the single end-of-row mark.
        # Anything else means it is not on an empty table cell,
        # or the range really does span more than the cell itself.
        # If this situation is detected,
        # copy and collapse the range, and fetch the content from that instead,
        # As a collapsed range on an empty cell does correctly return the table cell as its first enclosing element.
        if not self.isCollapsed:
            rawText = self._rangeObj.GetText(2)
            if not rawText or rawText == END_OF_ROW_MARK:
                r = self.copy()
                r.end = r.start
                fields = super(WordDocumentTextInfo,
                               r).getTextWithFields(formatConfig=formatConfig)
        if fields is None:
            fields = super().getTextWithFields(formatConfig=formatConfig)
        if len(fields) == 0:
            # Nothing to do... was probably a collapsed range.
            return fields

        # MS Word tries to produce speakable math content within equations.
        # However, using mathPlayer with the exposed mathml property on the equation is much nicer.
        # But, we therefore need to remove the inner math content if reading by line
        if not formatConfig or not formatConfig.get('extraDetail'):
            # We really only want to remove content if we can guarantee that mathPlayer is available.
            mathPres.ensureInit()
            if mathPres.speechProvider or mathPres.brailleProvider:
                curLevel = 0
                mathLevel = None
                mathStartIndex = None
                mathEndIndex = None
                for index in range(len(fields)):
                    field = fields[index]
                    if isinstance(field, textInfos.FieldCommand
                                  ) and field.command == "controlStart":
                        curLevel += 1
                        if mathLevel is None and field.field.get('mathml'):
                            mathLevel = curLevel
                            mathStartIndex = index
                    elif isinstance(field, textInfos.FieldCommand
                                    ) and field.command == "controlEnd":
                        if curLevel == mathLevel:
                            mathEndIndex = index
                        curLevel -= 1
                if mathEndIndex is not None:
                    del fields[mathStartIndex + 1:mathEndIndex]

        # Sometimes embedded objects and graphics In MS Word can cause a controlStart then a controlEnd with no actual formatChange / text in the middle.
        # SpeakTextInfo always expects that the first lot of controlStarts will always contain some text.
        # Therefore ensure that the first lot of controlStarts does contain some text by inserting a blank formatChange and empty string in this case.
        for index in range(len(fields)):
            field = fields[index]
            if isinstance(field, textInfos.FieldCommand
                          ) and field.command == "controlStart":
                continue
            elif isinstance(
                    field,
                    textInfos.FieldCommand) and field.command == "controlEnd":
                formatChange = textInfos.FieldCommand("formatChange",
                                                      textInfos.FormatField())
                fields.insert(index, formatChange)
                fields.insert(index + 1, "")
            break
        ##7971: Microsoft Word exposes list bullets as part of the actual text.
        # This then confuses NVDA's braille cursor routing as it expects that there is a one-to-one mapping between characters in the text string and   unit character moves.
        # Therefore, detect when at the start of a list, and strip the bullet from the text string, placing it in the text's formatField as line-prefix.
        listItemStarted = False
        lastFormatField = None
        for index in range(len(fields)):
            field = fields[index]
            if isinstance(field, textInfos.FieldCommand
                          ) and field.command == "controlStart":
                if field.field.get(
                        'role'
                ) == controlTypes.Role.LISTITEM and field.field.get(
                        '_startOfNode'):
                    # We are in the start of a list item.
                    listItemStarted = True
            elif isinstance(field, textInfos.FieldCommand
                            ) and field.command == "formatChange":
                # This is the most recent formatField we have seen.
                lastFormatField = field.field
            elif listItemStarted and isinstance(field, str):
                # This is the first text string within the list.
                # Remove the text up to the first space, and store it as line-prefix which NVDA will appropriately speak/braille as a bullet.
                try:
                    spaceIndex = field.index(' ')
                except ValueError:
                    log.debugWarning("No space found in this text string")
                    break
                prefix = field[0:spaceIndex]
                fields[index] = field[spaceIndex + 1:]
                lastFormatField['line-prefix'] = prefix
                # Let speech know that line-prefix is safe to be spoken always, as it will only be exposed on the very first formatField on the list item.
                lastFormatField['line-prefix_speakAlways'] = True
                break
            else:
                # Not a controlStart, formatChange or text string. Nothing to do.
                break
        # Fill in page number attributes where NVDA expects
        try:
            page = fields[0].field['page-number']
        except KeyError:
            page = None
        if page is not None:
            for field in fields:
                if isinstance(field, textInfos.FieldCommand) and isinstance(
                        field.field, textInfos.FormatField):
                    field.field['page-number'] = page
        # MS Word can sometimes return a higher ancestor in its textRange's children.
        # E.g. a table inside a table header.
        # This does not cause a loop, but does cause information to be doubled
        # Detect these duplicates and remove them from the generated fields.
        seenStarts = set()
        pendingRemoves = []
        index = 0
        for index, field in enumerate(fields):
            if isinstance(field, textInfos.FieldCommand
                          ) and field.command == "controlStart":
                runtimeID = field.field['runtimeID']
                if not runtimeID:
                    continue
                if runtimeID in seenStarts:
                    pendingRemoves.append(field.field)
                else:
                    seenStarts.add(runtimeID)
            elif seenStarts:
                seenStarts.clear()
        index = 0
        while index < len(fields):
            field = fields[index]
            if isinstance(field, textInfos.FieldCommand) and any(
                    x is field.field for x in pendingRemoves):
                del fields[index]
            else:
                index += 1
        return fields