<gh_stars>1-10
"""Provide a generic class for template-based file export.

All file representations with template-based write methods inherit from this class.

Copyright (c) 2021 <NAME>
For further information see https://github.com/peter88213/PyWriter
Published under the MIT License (https://opensource.org/licenses/mit-license.php)
"""
import os
from string import Template

from pywriter.model.character import Character
from pywriter.model.scene import Scene
from pywriter.model.novel import Novel
from pywriter.file.filter import Filter


class FileExport(Novel):
    """Abstract yWriter project file exporter representation.
    This class is generic and contains no conversion algorithm and no templates.
    """
    SUFFIX = ''

    fileHeader = ''
    partTemplate = ''
    chapterTemplate = ''
    notesChapterTemplate = ''
    todoChapterTemplate = ''
    unusedChapterTemplate = ''
    notExportedChapterTemplate = ''
    sceneTemplate = ''
    firstSceneTemplate = ''
    appendedSceneTemplate = ''
    notesSceneTemplate = ''
    todoSceneTemplate = ''
    unusedSceneTemplate = ''
    notExportedSceneTemplate = ''
    sceneDivider = ''
    chapterEndTemplate = ''
    unusedChapterEndTemplate = ''
    notExportedChapterEndTemplate = ''
    notesChapterEndTemplate = ''
    characterSectionHeading = ''
    characterTemplate = ''
    locationSectionHeading = ''
    locationTemplate = ''
    itemSectionHeading = ''
    itemTemplate = ''
    fileFooter = ''

    def __init__(self, filePath, **kwargs):
        """Extend the superclass constructor,
        initializing a filter class.
        """
        Novel.__init__(self, filePath, **kwargs)
        self.sceneFilter = Filter()
        self.chapterFilter = Filter()
        self.characterFilter = Filter()
        self.locationFilter = Filter()
        self.itemFilter = Filter()

    def merge(self, source):
        """Copy required attributes of the source object.
        Return a message beginning with SUCCESS or ERROR.
        """

        if source.title is not None:
            self.title = source.title

        else:
            self.title = ''

        if source.desc is not None:
            self.desc = source.desc

        else:
            self.desc = ''

        if source.author is not None:
            self.author = source.author

        else:
            self.author = ''

        if source.fieldTitle1 is not None:
            self.fieldTitle1 = source.fieldTitle1

        else:
            self.fieldTitle1 = 'Field 1'

        if source.fieldTitle2 is not None:
            self.fieldTitle2 = source.fieldTitle2

        else:
            self.fieldTitle2 = 'Field 2'

        if source.fieldTitle3 is not None:
            self.fieldTitle3 = source.fieldTitle3

        else:
            self.fieldTitle3 = 'Field 3'

        if source.fieldTitle4 is not None:
            self.fieldTitle4 = source.fieldTitle4

        else:
            self.fieldTitle4 = 'Field 4'

        if source.srtChapters != []:
            self.srtChapters = source.srtChapters

        if source.scenes is not None:
            self.scenes = source.scenes

        if source.chapters is not None:
            self.chapters = source.chapters

        if source.srtCharacters != []:
            self.srtCharacters = source.srtCharacters
            self.characters = source.characters

        if source.srtLocations != []:
            self.srtLocations = source.srtLocations
            self.locations = source.locations

        if source.srtItems != []:
            self.srtItems = source.srtItems
            self.items = source.items

        return 'SUCCESS'

    def get_fileHeaderMapping(self):
        """Return a mapping dictionary for the project section. 
        """
        projectTemplateMapping = dict(
            Title=self.title,
            Desc=self.convert_from_yw(self.desc),
            AuthorName=self.author,
            FieldTitle1=self.fieldTitle1,
            FieldTitle2=self.fieldTitle2,
            FieldTitle3=self.fieldTitle3,
            FieldTitle4=self.fieldTitle4,
        )
        return projectTemplateMapping

    def get_chapterMapping(self, chId, chapterNumber):
        """Return a mapping dictionary for a chapter section. 
        """
        if chapterNumber == 0:
            chapterNumber = ''

        chapterMapping = dict(
            ID=chId,
            ChapterNumber=chapterNumber,
            Title=self.chapters[chId].get_title(),
            Desc=self.convert_from_yw(self.chapters[chId].desc),
            ProjectName=self.projectName,
            ProjectPath=self.projectPath,
        )
        return chapterMapping

    def get_sceneMapping(self, scId, sceneNumber, wordsTotal, lettersTotal):
        """Return a mapping dictionary for a scene section. 
        """
        # Create a comma separated tag list.

        if sceneNumber == 0:
            sceneNumber = ''

        if self.scenes[scId].tags is not None:
            tags = self.get_string(self.scenes[scId].tags)

        else:
            tags = ''

        # Create a comma separated character list.

        try:
            # Note: Due to a bug, yWriter scenes might hold invalid
            # viepoint characters

            sChList = []

            for chId in self.scenes[scId].characters:
                sChList.append(self.characters[chId].title)

            sceneChars = self.get_string(sChList)
            viewpointChar = sChList[0]

        except:
            sceneChars = ''
            viewpointChar = ''

        # Create a comma separated location list.

        if self.scenes[scId].locations is not None:
            sLcList = []

            for lcId in self.scenes[scId].locations:
                sLcList.append(self.locations[lcId].title)

            sceneLocs = self.get_string(sLcList)

        else:
            sceneLocs = ''

        # Create a comma separated item list.

        if self.scenes[scId].items is not None:
            sItList = []

            for itId in self.scenes[scId].items:
                sItList.append(self.items[itId].title)

            sceneItems = self.get_string(sItList)

        else:
            sceneItems = ''

        # Create A/R marker string.

        if self.scenes[scId].isReactionScene:
            reactionScene = Scene.REACTION_MARKER

        else:
            reactionScene = Scene.ACTION_MARKER

        # Create a combined date information.

        if self.scenes[scId].date is not None and self.scenes[scId].date != Scene.NULL_DATE:
            day = ''
            date = self.scenes[scId].date
            scDate = self.scenes[scId].date

        else:
            date = ''

            if self.scenes[scId].day is not None:
                day = self.scenes[scId].day
                scDate = 'Day ' + self.scenes[scId].day

            else:
                day = ''
                scDate = ''

        # Create a combined time information.

        if self.scenes[scId].time is not None and self.scenes[scId].date != Scene.NULL_DATE:
            hour = ''
            minute = ''
            time = self.scenes[scId].time
            scTime = self.scenes[scId].time.rsplit(':', 1)[0]

        else:
            time = ''

            if self.scenes[scId].hour or self.scenes[scId].minute:

                if self.scenes[scId].hour:
                    hour = self.scenes[scId].hour

                else:
                    hour = '00'

                if self.scenes[scId].minute:
                    minute = self.scenes[scId].minute

                else:
                    minute = '00'

                scTime = hour.zfill(2) + ':' + minute.zfill(2)

            else:
                hour = ''
                minute = ''
                scTime = ''

        # Create a combined duration information.

        if self.scenes[scId].lastsDays is not None and self.scenes[scId].lastsDays != '0':
            lastsDays = self.scenes[scId].lastsDays
            days = self.scenes[scId].lastsDays + 'd '

        else:
            lastsDays = ''
            days = ''

        if self.scenes[scId].lastsHours is not None and self.scenes[scId].lastsHours != '0':
            lastsHours = self.scenes[scId].lastsHours
            hours = self.scenes[scId].lastsHours + 'h '

        else:
            lastsHours = ''
            hours = ''

        if self.scenes[scId].lastsMinutes is not None and self.scenes[scId].lastsMinutes != '0':
            lastsMinutes = self.scenes[scId].lastsMinutes
            minutes = self.scenes[scId].lastsMinutes + 'min'

        else:
            lastsMinutes = ''
            minutes = ''

        duration = days + hours + minutes

        sceneMapping = dict(
            ID=scId,
            SceneNumber=sceneNumber,
            Title=self.scenes[scId].title,
            Desc=self.convert_from_yw(self.scenes[scId].desc),
            WordCount=str(self.scenes[scId].wordCount),
            WordsTotal=wordsTotal,
            LetterCount=str(self.scenes[scId].letterCount),
            LettersTotal=lettersTotal,
            Status=Scene.STATUS[self.scenes[scId].status],
            SceneContent=self.convert_from_yw(self.scenes[scId].sceneContent),
            FieldTitle1=self.fieldTitle1,
            FieldTitle2=self.fieldTitle2,
            FieldTitle3=self.fieldTitle3,
            FieldTitle4=self.fieldTitle4,
            Field1=self.scenes[scId].field1,
            Field2=self.scenes[scId].field2,
            Field3=self.scenes[scId].field3,
            Field4=self.scenes[scId].field4,
            Date=date,
            Time=time,
            Day=day,
            Hour=hour,
            Minute=minute,
            ScDate=scDate,
            ScTime=scTime,
            LastsDays=lastsDays,
            LastsHours=lastsHours,
            LastsMinutes=lastsMinutes,
            Duration=duration,
            ReactionScene=reactionScene,
            Goal=self.convert_from_yw(self.scenes[scId].goal),
            Conflict=self.convert_from_yw(self.scenes[scId].conflict),
            Outcome=self.convert_from_yw(self.scenes[scId].outcome),
            Tags=tags,
            Image=self.scenes[scId].image,
            Characters=sceneChars,
            Viewpoint=viewpointChar,
            Locations=sceneLocs,
            Items=sceneItems,
            Notes=self.convert_from_yw(self.scenes[scId].sceneNotes),
            ProjectName=self.projectName,
            ProjectPath=self.projectPath,
        )

        return sceneMapping

    def get_characterMapping(self, crId):
        """Return a mapping dictionary for a character section. 
        """

        if self.characters[crId].tags is not None:
            tags = self.get_string(self.characters[crId].tags)

        else:
            tags = ''

        if self.characters[crId].isMajor:
            characterStatus = Character.MAJOR_MARKER

        else:
            characterStatus = Character.MINOR_MARKER

        characterMapping = dict(
            ID=crId,
            Title=self.characters[crId].title,
            Desc=self.convert_from_yw(self.characters[crId].desc),
            Tags=tags,
            Image=self.characters[crId].image,
            AKA=FileExport.convert_from_yw(self, self.characters[crId].aka),
            Notes=self.convert_from_yw(self.characters[crId].notes),
            Bio=self.convert_from_yw(self.characters[crId].bio),
            Goals=self.convert_from_yw(self.characters[crId].goals),
            FullName=FileExport.convert_from_yw(
                self, self.characters[crId].fullName),
            Status=characterStatus,
            ProjectName=self.projectName,
            ProjectPath=self.projectPath,
        )
        return characterMapping

    def get_locationMapping(self, lcId):
        """Return a mapping dictionary for a location section. 
        """

        if self.locations[lcId].tags is not None:
            tags = self.get_string(self.locations[lcId].tags)

        else:
            tags = ''

        locationMapping = dict(
            ID=lcId,
            Title=self.locations[lcId].title,
            Desc=self.convert_from_yw(self.locations[lcId].desc),
            Tags=tags,
            Image=self.locations[lcId].image,
            AKA=FileExport.convert_from_yw(self, self.locations[lcId].aka),
            ProjectName=self.projectName,
            ProjectPath=self.projectPath,
        )
        return locationMapping

    def get_itemMapping(self, itId):
        """Return a mapping dictionary for an item section. 
        """

        if self.items[itId].tags is not None:
            tags = self.get_string(self.items[itId].tags)

        else:
            tags = ''

        itemMapping = dict(
            ID=itId,
            Title=self.items[itId].title,
            Desc=self.convert_from_yw(self.items[itId].desc),
            Tags=tags,
            Image=self.items[itId].image,
            AKA=FileExport.convert_from_yw(self, self.items[itId].aka),
            ProjectName=self.projectName,
            ProjectPath=self.projectPath,
        )
        return itemMapping

    def get_fileHeader(self):
        """Process the file header.
        Return a list of strings.
        """
        lines = []
        template = Template(self.fileHeader)
        lines.append(template.safe_substitute(
            self.get_fileHeaderMapping()))
        return lines

    def get_scenes(self, chId, sceneNumber, wordsTotal, lettersTotal, doNotExport):
        """Process the scenes.
        Return a list of strings.
        """
        lines = []
        firstSceneInChapter = True

        for scId in self.chapters[chId].srtScenes:
            dispNumber = 0

            if not self.sceneFilter.accept(self, scId):
                continue

            # The order counts; be aware that "Todo" and "Notes" scenes are
            # always unused.

            if self.scenes[scId].isTodoScene:

                if self.todoSceneTemplate != '':
                    template = Template(self.todoSceneTemplate)

                else:
                    continue

            elif self.scenes[scId].isNotesScene:
                # Scene is "Notes" type.

                if self.notesSceneTemplate != '':
                    template = Template(self.notesSceneTemplate)

                else:
                    continue

            elif self.scenes[scId].isUnused or self.chapters[chId].isUnused:

                if self.unusedSceneTemplate != '':
                    template = Template(self.unusedSceneTemplate)

                else:
                    continue

            elif self.chapters[chId].oldType == 1:
                # Scene is "Info" type (old file format).

                if self.notesSceneTemplate != '':
                    template = Template(self.notesSceneTemplate)

                else:
                    continue

            elif self.scenes[scId].doNotExport or doNotExport:

                if self.notExportedSceneTemplate != '':
                    template = Template(self.notExportedSceneTemplate)

                else:
                    continue

            else:
                sceneNumber += 1
                dispNumber = sceneNumber
                wordsTotal += self.scenes[scId].wordCount
                lettersTotal += self.scenes[scId].letterCount

                template = Template(self.sceneTemplate)

                if not firstSceneInChapter and self.scenes[scId].appendToPrev and self.appendedSceneTemplate != '':
                    template = Template(self.appendedSceneTemplate)

            if not (firstSceneInChapter or self.scenes[scId].appendToPrev):
                lines.append(self.sceneDivider)

            if firstSceneInChapter and self.firstSceneTemplate != '':
                template = Template(self.firstSceneTemplate)

            lines.append(template.safe_substitute(self.get_sceneMapping(
                scId, dispNumber, wordsTotal, lettersTotal)))

            firstSceneInChapter = False

        return lines, sceneNumber, wordsTotal, lettersTotal

    def get_chapters(self):
        """Process the chapters and nested scenes.
        Return a list of strings.
        """
        lines = []
        chapterNumber = 0
        sceneNumber = 0
        wordsTotal = 0
        lettersTotal = 0

        for chId in self.srtChapters:
            dispNumber = 0

            if not self.chapterFilter.accept(self, chId):
                continue

            # The order counts; be aware that "Todo" and "Notes" chapters are
            # always unused.

            # Has the chapter only scenes not to be exported?

            sceneCount = 0
            notExportCount = 0
            doNotExport = False
            template = None

            for scId in self.chapters[chId].srtScenes:
                sceneCount += 1

                if self.scenes[scId].doNotExport:
                    notExportCount += 1

            if sceneCount > 0 and notExportCount == sceneCount:
                doNotExport = True

            if self.chapters[chId].chType == 2:
                # Chapter is "ToDo" type (implies "unused").

                if self.todoChapterTemplate != '':
                    template = Template(self.todoChapterTemplate)

            elif self.chapters[chId].chType == 1:
                # Chapter is "Notes" type (implies "unused").

                if self.notesChapterTemplate != '':
                    template = Template(self.notesChapterTemplate)

            elif self.chapters[chId].isUnused:
                # Chapter is "really" unused.

                if self.unusedChapterTemplate != '':
                    template = Template(self.unusedChapterTemplate)

            elif self.chapters[chId].oldType == 1:
                # Chapter is "Info" type (old file format).

                if self.notesChapterTemplate != '':
                    template = Template(self.notesChapterTemplate)

            elif doNotExport:

                if self.notExportedChapterTemplate != '':
                    template = Template(self.notExportedChapterTemplate)

            elif self.chapters[chId].chLevel == 1 and self.partTemplate != '':
                template = Template(self.partTemplate)

            else:
                template = Template(self.chapterTemplate)
                chapterNumber += 1
                dispNumber = chapterNumber

            if template is not None:
                lines.append(template.safe_substitute(self.get_chapterMapping(chId, dispNumber)))

            # Process scenes.

            sceneLines, sceneNumber, wordsTotal, lettersTotal = self.get_scenes(
                chId, sceneNumber, wordsTotal, lettersTotal, doNotExport)
            lines.extend(sceneLines)

            # Process chapter ending.

            template = None

            if self.chapters[chId].chType == 2:

                if self.todoChapterEndTemplate != '':
                    template = Template(self.todoChapterEndTemplate)

            elif self.chapters[chId].chType == 1:

                if self.notesChapterEndTemplate != '':
                    template = Template(self.notesChapterEndTemplate)

            elif self.chapters[chId].isUnused:

                if self.unusedChapterEndTemplate != '':
                    template = Template(self.unusedChapterEndTemplate)

            elif self.chapters[chId].oldType == 1:

                if self.notesChapterEndTemplate != '':
                    template = Template(self.notesChapterEndTemplate)

            elif doNotExport:

                if self.notExportedChapterEndTemplate != '':
                    template = Template(self.notExportedChapterEndTemplate)

            elif self.chapterEndTemplate != '':
                template = Template(self.chapterEndTemplate)

            if template is not None:
                lines.append(template.safe_substitute(self.get_chapterMapping(chId, dispNumber)))

        return lines

    def get_characters(self):
        """Process the characters.
        Return a list of strings.
        """

        if self.characterSectionHeading:
            lines = [self.characterSectionHeading]

        else:
            lines = []

        template = Template(self.characterTemplate)

        for crId in self.srtCharacters:

            if self.characterFilter.accept(self, crId):
                lines.append(template.safe_substitute(self.get_characterMapping(crId)))

        return lines

    def get_locations(self):
        """Process the locations.
        Return a list of strings.
        """

        if self.locationSectionHeading:
            lines = [self.locationSectionHeading]

        else:
            lines = []

        template = Template(self.locationTemplate)

        for lcId in self.srtLocations:

            if self.locationFilter.accept(self, lcId):
                lines.append(template.safe_substitute(self.get_locationMapping(lcId)))

        return lines

    def get_items(self):
        """Process the items.
        Return a list of strings.
        """

        if self.itemSectionHeading:
            lines = [self.itemSectionHeading]

        else:
            lines = []

        template = Template(self.itemTemplate)

        for itId in self.srtItems:

            if self.itemFilter.accept(self, itId):
                lines.append(template.safe_substitute(self.get_itemMapping(itId)))

        return lines

    def get_text(self):
        """Assemple the whole text applying the templates.
        Return a string to be written to the output file.
        """
        lines = self.get_fileHeader()
        lines.extend(self.get_chapters())
        lines.extend(self.get_characters())
        lines.extend(self.get_locations())
        lines.extend(self.get_items())
        lines.append(self.fileFooter)
        return ''.join(lines)

    def write(self):
        """Create a template-based output file. 
        Return a message string starting with 'SUCCESS' or 'ERROR'.
        """
        text = self.get_text()

        try:
            with open(self.filePath, 'w', encoding='utf-8') as f:
                f.write(text)

        except:
            return 'ERROR: Cannot write "' + os.path.normpath(self.filePath) + '".'

        return 'SUCCESS: "' + os.path.normpath(self.filePath) + '" written.'

    def get_string(self, elements):
        """Return a string which is the concatenation of the 
        members of the list of strings "elements", separated by 
        a comma plus a space. The space allows word wrap in 
        spreadsheet cells.
        """
        text = (', ').join(elements)
        return text

    def convert_from_yw(self, text):
        """Convert yw7 markup to target format.
        This is a stub to be overridden by subclass methods.
        """

        if text is None:
            text = ''

        return(text)

"""Provide a generic class for template-based file export.

All file representations with template-based write methods inherit from this class.

Copyright (c) 2021 <NAME>
For further information see https://github.com/peter88213/PyWriter
Published under the MIT License (https://opensource.org/licenses/mit-license.php)
"""
import os
from string import Template

from pywriter.model.character import Character
from pywriter.model.scene import Scene
from pywriter.model.novel import Novel
from pywriter.file.filter import Filter


class FileExport(Novel):
    """Abstract yWriter project file exporter representation.
    This class is generic and contains no conversion algorithm and no templates.
    """
    SUFFIX = ''

    fileHeader = ''
    partTemplate = ''
    chapterTemplate = ''
    notesChapterTemplate = ''
    todoChapterTemplate = ''
    unusedChapterTemplate = ''
    notExportedChapterTemplate = ''
    sceneTemplate = ''
    firstSceneTemplate = ''
    appendedSceneTemplate = ''
    notesSceneTemplate = ''
    todoSceneTemplate = ''
    unusedSceneTemplate = ''
    notExportedSceneTemplate = ''
    sceneDivider = ''
    chapterEndTemplate = ''
    unusedChapterEndTemplate = ''
    notExportedChapterEndTemplate = ''
    notesChapterEndTemplate = ''
    characterSectionHeading = ''
    characterTemplate = ''
    locationSectionHeading = ''
    locationTemplate = ''
    itemSectionHeading = ''
    itemTemplate = ''
    fileFooter = ''

    def __init__(self, filePath, **kwargs):
        """Extend the superclass constructor,
        initializing a filter class.
        """
        Novel.__init__(self, filePath, **kwargs)
        self.sceneFilter = Filter()
        self.chapterFilter = Filter()
        self.characterFilter = Filter()
        self.locationFilter = Filter()
        self.itemFilter = Filter()

    def merge(self, source):
        """Copy required attributes of the source object.
        Return a message beginning with SUCCESS or ERROR.
        """

        if source.title is not None:
            self.title = source.title

        else:
            self.title = ''

        if source.desc is not None:
            self.desc = source.desc

        else:
            self.desc = ''

        if source.author is not None:
            self.author = source.author

        else:
            self.author = ''

        if source.fieldTitle1 is not None:
            self.fieldTitle1 = source.fieldTitle1

        else:
            self.fieldTitle1 = 'Field 1'

        if source.fieldTitle2 is not None:
            self.fieldTitle2 = source.fieldTitle2

        else:
            self.fieldTitle2 = 'Field 2'

        if source.fieldTitle3 is not None:
            self.fieldTitle3 = source.fieldTitle3

        else:
            self.fieldTitle3 = 'Field 3'

        if source.fieldTitle4 is not None:
            self.fieldTitle4 = source.fieldTitle4

        else:
            self.fieldTitle4 = 'Field 4'

        if source.srtChapters != []:
            self.srtChapters = source.srtChapters

        if source.scenes is not None:
            self.scenes = source.scenes

        if source.chapters is not None:
            self.chapters = source.chapters

        if source.srtCharacters != []:
            self.srtCharacters = source.srtCharacters
            self.characters = source.characters

        if source.srtLocations != []:
            self.srtLocations = source.srtLocations
            self.locations = source.locations

        if source.srtItems != []:
            self.srtItems = source.srtItems
            self.items = source.items

        return 'SUCCESS'

    def get_fileHeaderMapping(self):
        """Return a mapping dictionary for the project section. 
        """
        projectTemplateMapping = dict(
            Title=self.title,
            Desc=self.convert_from_yw(self.desc),
            AuthorName=self.author,
            FieldTitle1=self.fieldTitle1,
            FieldTitle2=self.fieldTitle2,
            FieldTitle3=self.fieldTitle3,
            FieldTitle4=self.fieldTitle4,
        )
        return projectTemplateMapping

    def get_chapterMapping(self, chId, chapterNumber):
        """Return a mapping dictionary for a chapter section. 
        """
        if chapterNumber == 0:
            chapterNumber = ''

        chapterMapping = dict(
            ID=chId,
            ChapterNumber=chapterNumber,
            Title=self.chapters[chId].get_title(),
            Desc=self.convert_from_yw(self.chapters[chId].desc),
            ProjectName=self.projectName,
            ProjectPath=self.projectPath,
        )
        return chapterMapping

    def get_sceneMapping(self, scId, sceneNumber, wordsTotal, lettersTotal):
        """Return a mapping dictionary for a scene section. 
        """
        # Create a comma separated tag list.

        if sceneNumber == 0:
            sceneNumber = ''

        if self.scenes[scId].tags is not None:
            tags = self.get_string(self.scenes[scId].tags)

        else:
            tags = ''

        # Create a comma separated character list.

        try:
            # Note: Due to a bug, yWriter scenes might hold invalid
            # viepoint characters

            sChList = []

            for chId in self.scenes[scId].characters:
                sChList.append(self.characters[chId].title)

            sceneChars = self.get_string(sChList)
            viewpointChar = sChList[0]

        except:
            sceneChars = ''
            viewpointChar = ''

        # Create a comma separated location list.

        if self.scenes[scId].locations is not None:
            sLcList = []

            for lcId in self.scenes[scId].locations:
                sLcList.append(self.locations[lcId].title)

            sceneLocs = self.get_string(sLcList)

        else:
            sceneLocs = ''

        # Create a comma separated item list.

        if self.scenes[scId].items is not None:
            sItList = []

            for itId in self.scenes[scId].items:
                sItList.append(self.items[itId].title)

            sceneItems = self.get_string(sItList)

        else:
            sceneItems = ''

        # Create A/R marker string.

        if self.scenes[scId].isReactionScene:
            reactionScene = Scene.REACTION_MARKER

        else:
            reactionScene = Scene.ACTION_MARKER

        # Create a combined date information.

        if self.scenes[scId].date is not None and self.scenes[scId].date != Scene.NULL_DATE:
            day = ''
            date = self.scenes[scId].date
            scDate = self.scenes[scId].date

        else:
            date = ''

            if self.scenes[scId].day is not None:
                day = self.scenes[scId].day
                scDate = 'Day ' + self.scenes[scId].day

            else:
                day = ''
                scDate = ''

        # Create a combined time information.

        if self.scenes[scId].time is not None and self.scenes[scId].date != Scene.NULL_DATE:
            hour = ''
            minute = ''
            time = self.scenes[scId].time
            scTime = self.scenes[scId].time.rsplit(':', 1)[0]

        else:
            time = ''

            if self.scenes[scId].hour or self.scenes[scId].minute:

                if self.scenes[scId].hour:
                    hour = self.scenes[scId].hour

                else:
                    hour = '00'

                if self.scenes[scId].minute:
                    minute = self.scenes[scId].minute

                else:
                    minute = '00'

                scTime = hour.zfill(2) + ':' + minute.zfill(2)

            else:
                hour = ''
                minute = ''
                scTime = ''

        # Create a combined duration information.

        if self.scenes[scId].lastsDays is not None and self.scenes[scId].lastsDays != '0':
            lastsDays = self.scenes[scId].lastsDays
            days = self.scenes[scId].lastsDays + 'd '

        else:
            lastsDays = ''
            days = ''

        if self.scenes[scId].lastsHours is not None and self.scenes[scId].lastsHours != '0':
            lastsHours = self.scenes[scId].lastsHours
            hours = self.scenes[scId].lastsHours + 'h '

        else:
            lastsHours = ''
            hours = ''

        if self.scenes[scId].lastsMinutes is not None and self.scenes[scId].lastsMinutes != '0':
            lastsMinutes = self.scenes[scId].lastsMinutes
            minutes = self.scenes[scId].lastsMinutes + 'min'

        else:
            lastsMinutes = ''
            minutes = ''

        duration = days + hours + minutes

        sceneMapping = dict(
            ID=scId,
            SceneNumber=sceneNumber,
            Title=self.scenes[scId].title,
            Desc=self.convert_from_yw(self.scenes[scId].desc),
            WordCount=str(self.scenes[scId].wordCount),
            WordsTotal=wordsTotal,
            LetterCount=str(self.scenes[scId].letterCount),
            LettersTotal=lettersTotal,
            Status=Scene.STATUS[self.scenes[scId].status],
            SceneContent=self.convert_from_yw(self.scenes[scId].sceneContent),
            FieldTitle1=self.fieldTitle1,
            FieldTitle2=self.fieldTitle2,
            FieldTitle3=self.fieldTitle3,
            FieldTitle4=self.fieldTitle4,
            Field1=self.scenes[scId].field1,
            Field2=self.scenes[scId].field2,
            Field3=self.scenes[scId].field3,
            Field4=self.scenes[scId].field4,
            Date=date,
            Time=time,
            Day=day,
            Hour=hour,
            Minute=minute,
            ScDate=scDate,
            ScTime=scTime,
            LastsDays=lastsDays,
            LastsHours=lastsHours,
            LastsMinutes=lastsMinutes,
            Duration=duration,
            ReactionScene=reactionScene,
            Goal=self.convert_from_yw(self.scenes[scId].goal),
            Conflict=self.convert_from_yw(self.scenes[scId].conflict),
            Outcome=self.convert_from_yw(self.scenes[scId].outcome),
            Tags=tags,
            Image=self.scenes[scId].image,
            Characters=sceneChars,
            Viewpoint=viewpointChar,
            Locations=sceneLocs,
            Items=sceneItems,
            Notes=self.convert_from_yw(self.scenes[scId].sceneNotes),
            ProjectName=self.projectName,
            ProjectPath=self.projectPath,
        )

        return sceneMapping

    def get_characterMapping(self, crId):
        """Return a mapping dictionary for a character section. 
        """

        if self.characters[crId].tags is not None:
            tags = self.get_string(self.characters[crId].tags)

        else:
            tags = ''

        if self.characters[crId].isMajor:
            characterStatus = Character.MAJOR_MARKER

        else:
            characterStatus = Character.MINOR_MARKER

        characterMapping = dict(
            ID=crId,
            Title=self.characters[crId].title,
            Desc=self.convert_from_yw(self.characters[crId].desc),
            Tags=tags,
            Image=self.characters[crId].image,
            AKA=FileExport.convert_from_yw(self, self.characters[crId].aka),
            Notes=self.convert_from_yw(self.characters[crId].notes),
            Bio=self.convert_from_yw(self.characters[crId].bio),
            Goals=self.convert_from_yw(self.characters[crId].goals),
            FullName=FileExport.convert_from_yw(
                self, self.characters[crId].fullName),
            Status=characterStatus,
            ProjectName=self.projectName,
            ProjectPath=self.projectPath,
        )
        return characterMapping

    def get_locationMapping(self, lcId):
        """Return a mapping dictionary for a location section. 
        """

        if self.locations[lcId].tags is not None:
            tags = self.get_string(self.locations[lcId].tags)

        else:
            tags = ''

        locationMapping = dict(
            ID=lcId,
            Title=self.locations[lcId].title,
            Desc=self.convert_from_yw(self.locations[lcId].desc),
            Tags=tags,
            Image=self.locations[lcId].image,
            AKA=FileExport.convert_from_yw(self, self.locations[lcId].aka),
            ProjectName=self.projectName,
            ProjectPath=self.projectPath,
        )
        return locationMapping

    def get_itemMapping(self, itId):
        """Return a mapping dictionary for an item section. 
        """

        if self.items[itId].tags is not None:
            tags = self.get_string(self.items[itId].tags)

        else:
            tags = ''

        itemMapping = dict(
            ID=itId,
            Title=self.items[itId].title,
            Desc=self.convert_from_yw(self.items[itId].desc),
            Tags=tags,
            Image=self.items[itId].image,
            AKA=FileExport.convert_from_yw(self, self.items[itId].aka),
            ProjectName=self.projectName,
            ProjectPath=self.projectPath,
        )
        return itemMapping

    def get_fileHeader(self):
        """Process the file header.
        Return a list of strings.
        """
        lines = []
        template = Template(self.fileHeader)
        lines.append(template.safe_substitute(
            self.get_fileHeaderMapping()))
        return lines

    def get_scenes(self, chId, sceneNumber, wordsTotal, lettersTotal, doNotExport):
        """Process the scenes.
        Return a list of strings.
        """
        lines = []
        firstSceneInChapter = True

        for scId in self.chapters[chId].srtScenes:
            dispNumber = 0

            if not self.sceneFilter.accept(self, scId):
                continue

            # The order counts; be aware that "Todo" and "Notes" scenes are
            # always unused.

            if self.scenes[scId].isTodoScene:

                if self.todoSceneTemplate != '':
                    template = Template(self.todoSceneTemplate)

                else:
                    continue

            elif self.scenes[scId].isNotesScene:
                # Scene is "Notes" type.

                if self.notesSceneTemplate != '':
                    template = Template(self.notesSceneTemplate)

                else:
                    continue

            elif self.scenes[scId].isUnused or self.chapters[chId].isUnused:

                if self.unusedSceneTemplate != '':
                    template = Template(self.unusedSceneTemplate)

                else:
                    continue

            elif self.chapters[chId].oldType == 1:
                # Scene is "Info" type (old file format).

                if self.notesSceneTemplate != '':
                    template = Template(self.notesSceneTemplate)

                else:
                    continue

            elif self.scenes[scId].doNotExport or doNotExport:

                if self.notExportedSceneTemplate != '':
                    template = Template(self.notExportedSceneTemplate)

                else:
                    continue

            else:
                sceneNumber += 1
                dispNumber = sceneNumber
                wordsTotal += self.scenes[scId].wordCount
                lettersTotal += self.scenes[scId].letterCount

                template = Template(self.sceneTemplate)

                if not firstSceneInChapter and self.scenes[scId].appendToPrev and self.appendedSceneTemplate != '':
                    template = Template(self.appendedSceneTemplate)

            if not (firstSceneInChapter or self.scenes[scId].appendToPrev):
                lines.append(self.sceneDivider)

            if firstSceneInChapter and self.firstSceneTemplate != '':
                template = Template(self.firstSceneTemplate)

            lines.append(template.safe_substitute(self.get_sceneMapping(
                scId, dispNumber, wordsTotal, lettersTotal)))

            firstSceneInChapter = False

        return lines, sceneNumber, wordsTotal, lettersTotal

    def get_chapters(self):
        """Process the chapters and nested scenes.
        Return a list of strings.
        """
        lines = []
        chapterNumber = 0
        sceneNumber = 0
        wordsTotal = 0
        lettersTotal = 0

        for chId in self.srtChapters:
            dispNumber = 0

            if not self.chapterFilter.accept(self, chId):
                continue

            # The order counts; be aware that "Todo" and "Notes" chapters are
            # always unused.

            # Has the chapter only scenes not to be exported?

            sceneCount = 0
            notExportCount = 0
            doNotExport = False
            template = None

            for scId in self.chapters[chId].srtScenes:
                sceneCount += 1

                if self.scenes[scId].doNotExport:
                    notExportCount += 1

            if sceneCount > 0 and notExportCount == sceneCount:
                doNotExport = True

            if self.chapters[chId].chType == 2:
                # Chapter is "ToDo" type (implies "unused").

                if self.todoChapterTemplate != '':
                    template = Template(self.todoChapterTemplate)

            elif self.chapters[chId].chType == 1:
                # Chapter is "Notes" type (implies "unused").

                if self.notesChapterTemplate != '':
                    template = Template(self.notesChapterTemplate)

            elif self.chapters[chId].isUnused:
                # Chapter is "really" unused.

                if self.unusedChapterTemplate != '':
                    template = Template(self.unusedChapterTemplate)

            elif self.chapters[chId].oldType == 1:
                # Chapter is "Info" type (old file format).

                if self.notesChapterTemplate != '':
                    template = Template(self.notesChapterTemplate)

            elif doNotExport:

                if self.notExportedChapterTemplate != '':
                    template = Template(self.notExportedChapterTemplate)

            elif self.chapters[chId].chLevel == 1 and self.partTemplate != '':
                template = Template(self.partTemplate)

            else:
                template = Template(self.chapterTemplate)
                chapterNumber += 1
                dispNumber = chapterNumber

            if template is not None:
                lines.append(template.safe_substitute(self.get_chapterMapping(chId, dispNumber)))

            # Process scenes.

            sceneLines, sceneNumber, wordsTotal, lettersTotal = self.get_scenes(
                chId, sceneNumber, wordsTotal, lettersTotal, doNotExport)
            lines.extend(sceneLines)

            # Process chapter ending.

            template = None

            if self.chapters[chId].chType == 2:

                if self.todoChapterEndTemplate != '':
                    template = Template(self.todoChapterEndTemplate)

            elif self.chapters[chId].chType == 1:

                if self.notesChapterEndTemplate != '':
                    template = Template(self.notesChapterEndTemplate)

            elif self.chapters[chId].isUnused:

                if self.unusedChapterEndTemplate != '':
                    template = Template(self.unusedChapterEndTemplate)

            elif self.chapters[chId].oldType == 1:

                if self.notesChapterEndTemplate != '':
                    template = Template(self.notesChapterEndTemplate)

            elif doNotExport:

                if self.notExportedChapterEndTemplate != '':
                    template = Template(self.notExportedChapterEndTemplate)

            elif self.chapterEndTemplate != '':
                template = Template(self.chapterEndTemplate)

            if template is not None:
                lines.append(template.safe_substitute(self.get_chapterMapping(chId, dispNumber)))

        return lines

    def get_characters(self):
        """Process the characters.
        Return a list of strings.
        """

        if self.characterSectionHeading:
            lines = [self.characterSectionHeading]

        else:
            lines = []

        template = Template(self.characterTemplate)

        for crId in self.srtCharacters:

            if self.characterFilter.accept(self, crId):
                lines.append(template.safe_substitute(self.get_characterMapping(crId)))

        return lines

    def get_locations(self):
        """Process the locations.
        Return a list of strings.
        """

        if self.locationSectionHeading:
            lines = [self.locationSectionHeading]

        else:
            lines = []

        template = Template(self.locationTemplate)

        for lcId in self.srtLocations:

            if self.locationFilter.accept(self, lcId):
                lines.append(template.safe_substitute(self.get_locationMapping(lcId)))

        return lines

    def get_items(self):
        """Process the items.
        Return a list of strings.
        """

        if self.itemSectionHeading:
            lines = [self.itemSectionHeading]

        else:
            lines = []

        template = Template(self.itemTemplate)

        for itId in self.srtItems:

            if self.itemFilter.accept(self, itId):
                lines.append(template.safe_substitute(self.get_itemMapping(itId)))

        return lines

    def get_text(self):
        """Assemple the whole text applying the templates.
        Return a string to be written to the output file.
        """
        lines = self.get_fileHeader()
        lines.extend(self.get_chapters())
        lines.extend(self.get_characters())
        lines.extend(self.get_locations())
        lines.extend(self.get_items())
        lines.append(self.fileFooter)
        return ''.join(lines)

    def write(self):
        """Create a template-based output file. 
        Return a message string starting with 'SUCCESS' or 'ERROR'.
        """
        text = self.get_text()

        try:
            with open(self.filePath, 'w', encoding='utf-8') as f:
                f.write(text)

        except:
            return 'ERROR: Cannot write "' + os.path.normpath(self.filePath) + '".'

        return 'SUCCESS: "' + os.path.normpath(self.filePath) + '" written.'

    def get_string(self, elements):
        """Return a string which is the concatenation of the 
        members of the list of strings "elements", separated by 
        a comma plus a space. The space allows word wrap in 
        spreadsheet cells.
        """
        text = (', ').join(elements)
        return text

    def convert_from_yw(self, text):
        """Convert yw7 markup to target format.
        This is a stub to be overridden by subclass methods.
        """

        if text is None:
            text = ''

        return(text)
Provide a generic class for template-based file export.
 
 All file representations with template-based write methods inherit from this class.
 
 Copyright (c) 2021 <NAME>
 For further information see https://github.com/peter88213/PyWriter
 Published under the MIT License (https://opensource.org/licenses/mit-license.php) Abstract yWriter project file exporter representation.
     This class is generic and contains no conversion algorithm and no templates. Extend the superclass constructor,
         initializing a filter class. Copy required attributes of the source object.
         Return a message beginning with SUCCESS or ERROR. Return a mapping dictionary for the project section. Return a mapping dictionary for a chapter section. Return a mapping dictionary for a scene section. # Create a comma separated tag list. # Create a comma separated character list. # Note: Due to a bug, yWriter scenes might hold invalid # viepoint characters # Create a comma separated location list. # Create a comma separated item list. # Create A/R marker string. # Create a combined date information. # Create a combined time information. # Create a combined duration information. Return a mapping dictionary for a character section. Return a mapping dictionary for a location section. Return a mapping dictionary for an item section. Process the file header.
         Return a list of strings. Process the scenes.
         Return a list of strings. # The order counts; be aware that "Todo" and "Notes" scenes are # always unused. # Scene is "Notes" type. # Scene is "Info" type (old file format). Process the chapters and nested scenes.
         Return a list of strings. # The order counts; be aware that "Todo" and "Notes" chapters are # always unused. # Has the chapter only scenes not to be exported? # Chapter is "ToDo" type (implies "unused"). # Chapter is "Notes" type (implies "unused"). # Chapter is "really" unused. # Chapter is "Info" type (old file format). # Process scenes. # Process chapter ending. Process the characters.
         Return a list of strings. Process the locations.
         Return a list of strings. Process the items.
         Return a list of strings. Assemple the whole text applying the templates.
         Return a string to be written to the output file. Create a template-based output file. 
         Return a message string starting with 'SUCCESS' or 'ERROR'. Return a string which is the concatenation of the 
         members of the list of strings "elements", separated by 
         a comma plus a space. The space allows word wrap in 
         spreadsheet cells. Convert yw7 markup to target format.
         This is a stub to be overridden by subclass methods.