Given an iterable of stream fragments, write it to the stream object
    by using its write method.  Returns a 3-tuple, where the first
    element is the mapping, second element is the list of sources and
    the third being the original names referenced by the given fragment.

    Arguments:

    stream_fragments
        an iterable that only contains StreamFragments
    stream
        an io.IOBase compatible stream object
    normalize
        the default True setting will result in the mappings that were
        returned be normalized to the minimum form.  This will reduce
        the size of the generated source map at the expense of slightly
        lower quality.

        Also, if any of the subsequent arguments are provided (for
        instance, for the multiple calls to this function), the usage of
        the normalize flag is currently NOT supported.

        If multiple sets of outputs are to be produced, the recommended
        method is to chain all the stream fragments together before
        passing in.

    Advanced usage arguments

    book
        A Book instance; if none is provided an instance will be created
        from the default_book constructor.  The Bookkeeper instance is
        used for tracking the positions of rows and columns of the input
        stream.
    sources
        a Names instance for tracking sources; if None is provided, an
        instance will be created for internal use.
    names
        a Names instance for tracking names; if None is provided, an
        instance will be created for internal use.
    mappings
        a previously produced mappings.

    A stream fragment tuple must contain the following

    - The string to write to the stream
    - Original starting line of the string; None if not present
    - Original starting column fo the line; None if not present
    - Original string that this fragment represents (i.e. for the case
      where this string fragment was an identifier but got mangled into
      an alternative form); use None if this was not the case.
    - The source of the fragment.  If the first fragment is unspecified,
      the INVALID_SOURCE url will be used (i.e. about:invalid).  After
      that, a None value will be treated as the implicit value, and if
      NotImplemented is encountered, the INVALID_SOURCE url will be used
      also.

    If a number of stream_fragments are to be provided, common instances
    of Book (constructed via default_book) and Names (for sources and
    names) should be provided if they are not chained together.

def write(
        stream_fragments, stream, normalize=True,
        book=None, sources=None, names=None, mappings=None):
    """
    Given an iterable of stream fragments, write it to the stream object
    by using its write method.  Returns a 3-tuple, where the first
    element is the mapping, second element is the list of sources and
    the third being the original names referenced by the given fragment.

    Arguments:

    stream_fragments
        an iterable that only contains StreamFragments
    stream
        an io.IOBase compatible stream object
    normalize
        the default True setting will result in the mappings that were
        returned be normalized to the minimum form.  This will reduce
        the size of the generated source map at the expense of slightly
        lower quality.

        Also, if any of the subsequent arguments are provided (for
        instance, for the multiple calls to this function), the usage of
        the normalize flag is currently NOT supported.

        If multiple sets of outputs are to be produced, the recommended
        method is to chain all the stream fragments together before
        passing in.

    Advanced usage arguments

    book
        A Book instance; if none is provided an instance will be created
        from the default_book constructor.  The Bookkeeper instance is
        used for tracking the positions of rows and columns of the input
        stream.
    sources
        a Names instance for tracking sources; if None is provided, an
        instance will be created for internal use.
    names
        a Names instance for tracking names; if None is provided, an
        instance will be created for internal use.
    mappings
        a previously produced mappings.

    A stream fragment tuple must contain the following

    - The string to write to the stream
    - Original starting line of the string; None if not present
    - Original starting column fo the line; None if not present
    - Original string that this fragment represents (i.e. for the case
      where this string fragment was an identifier but got mangled into
      an alternative form); use None if this was not the case.
    - The source of the fragment.  If the first fragment is unspecified,
      the INVALID_SOURCE url will be used (i.e. about:invalid).  After
      that, a None value will be treated as the implicit value, and if
      NotImplemented is encountered, the INVALID_SOURCE url will be used
      also.

    If a number of stream_fragments are to be provided, common instances
    of Book (constructed via default_book) and Names (for sources and
    names) should be provided if they are not chained together.
    """

    def push_line():
        mappings.append([])
        book.keeper._sink_column = 0

    if names is None:
        names = Names()

    if sources is None:
        sources = Names()

    if book is None:
        book = default_book()

    if not isinstance(mappings, list):
        # note that
        mappings = []
        # finalize initial states; the most recent list (mappings[-1])
        # is the current line
        push_line()

    for chunk, lineno, colno, original_name, source in stream_fragments:
        # note that lineno/colno are assumed to be both provided or none
        # provided.
        lines = chunk.splitlines(True)
        for line in lines:
            stream.write(line)

            # Two separate checks are done.  As per specification, if
            # either lineno or colno are unspecified, it is assumed that
            # the segment is unmapped - append a termination (1-tuple)
            #
            # Otherwise, note that if this segment is the beginning of a
            # line, and that an implied source colno/linecol were
            # provided (i.e. value of 0), and that the string is empty,
            # it can be safely skipped, since it is an implied and
            # unmapped indentation

            if lineno is None or colno is None:
                mappings[-1].append((book.keeper.sink_column,))
            else:
                name_id = names.update(original_name)
                # this is a bit of a trick: an unspecified value (None)
                # will simply be treated as the implied value, hence 0.
                # However, a NotImplemented will be recorded and be
                # convereted to the invalid url at the end.
                source_id = sources.update(source) or 0

                if lineno:
                    # a new lineno is provided, apply it to the book and
                    # use the result as the written value.
                    book.keeper.source_line = lineno
                    source_line = book.keeper.source_line
                else:
                    # no change in offset, do not calculate and assume
                    # the value to be written is unchanged.
                    source_line = 0

                # if the provided colno is to be inferred, calculate it
                # based on the previous line length plus the previous
                # real source column value, otherwise standard value
                # for tracking.

                # the reason for using the previous lengths is simply
                # due to how the bookkeeper class does the calculation
                # on-demand, and that the starting column for the
                # _current_ text fragment can only be calculated using
                # what was written previously, hence the original length
                # value being added if the current colno is to be
                # inferred.
                if colno:
                    book.keeper.source_column = colno
                else:
                    book.keeper.source_column = (
                        book.keeper._source_column + book.original_len)

                if original_name is not None:
                    mappings[-1].append((
                        book.keeper.sink_column, source_id,
                        source_line, book.keeper.source_column,
                        name_id
                    ))
                else:
                    mappings[-1].append((
                        book.keeper.sink_column, source_id,
                        source_line, book.keeper.source_column
                    ))

            # doing this last to update the position for the next line
            # or chunk for the relative values based on what was added
            if line[-1:] in '\r\n':
                # Note: this HAS to be an edge case and should never
                # happen, but this has the potential to muck things up.
                # Since the parent only provided the start, will need
                # to manually track the chunks internal to here.
                # This normally shouldn't happen with sane parsers
                # and lexers, but this assumes that no further symbols
                # aside from the new lines got inserted.
                colno = (
                    colno if colno in (0, None) else
                    colno + len(line.rstrip()))
                book.original_len = book.written_len = 0
                push_line()

                if line is not lines[-1]:
                    logger.warning(
                        'text in the generated document at line %d may be '
                        'mapped incorrectly due to trailing newline character '
                        'in provided text fragment.', len(mappings)
                    )
                    logger.info(
                        'text in stream fragments should not have trailing '
                        'characters after a new line, they should be split '
                        'off into a separate fragment.'
                    )
            else:
                book.written_len = len(line)
                book.original_len = (
                    len(original_name) if original_name else book.written_len)
                book.keeper.sink_column = (
                    book.keeper._sink_column + book.written_len)

    # normalize everything
    if normalize:
        # if this _ever_ supports the multiple usage using existence
        # instances of names and book and mappings, it needs to deal
        # with NOT normalizing the existing mappings and somehow reuse
        # the previously stored value, probably in the book.  It is
        # most certainly a bad idea to support that use case while also
        # supporting the default normalize flag due to the complex
        # tracking of all the existing values...
        mappings = normalize_mappings(mappings)

    list_sources = [
        INVALID_SOURCE if s == NotImplemented else s for s in sources
    ] or [INVALID_SOURCE]
    return mappings, list_sources, list(names)