/*
 * Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define */

define({

    /**
     * Errors
     */

    // General file io error strings
    "GENERIC_ERROR"                     : "(fejl {0})",
    "NOT_FOUND_ERR"                     : "Filen kunne ikke findes.",
    "NOT_READABLE_ERR"                  : "Filen kunne ikke læses.",
    "NO_MODIFICATION_ALLOWED_ERR"       : "Den pågældende mappe kan ikke ændres.",
    "NO_MODIFICATION_ALLOWED_ERR_FILE"  : "Du har ikke tilladelse til at lave ændringer.",
    "CONTENTS_MODIFIED_ERR"             : "Filen er blevet ændret udenfor {APP_NAME}.",
    "UNSUPPORTED_ENCODING_ERR"          : "{APP_NAME} understøtter i øjeblikket kun tekstfiler kodet i UTF-8.",
    "FILE_EXISTS_ERR"                   : "Filen eller mappen eksisterer allerede.",
    "FILE"                              : "filen",
    "DIRECTORY"                         : "mappen",
    "DIRECTORY_NAMES_LEDE"              : "Mappe navne",
    "FILENAMES_LEDE"                    : "Filnavne",
    "FILENAME"                          : "filnavn",
    "DIRECTORY_NAME"                    : "mappenavn",

    // Project error strings
    "ERROR_LOADING_PROJECT"             : "Fejl ved indlæsning af projekt",
    "OPEN_DIALOG_ERROR"                 : "Der skete en fejl ved visning af \"Åbn fil\"-dialogboksen. (fejl {0})",
    "REQUEST_NATIVE_FILE_SYSTEM_ERROR"  : "Der skete en fejl i forsøget på at læse mappen {0}. (fejl {1})",
    "READ_DIRECTORY_ENTRIES_ERROR"      : "Der skete en fejl ved læsning af indholdet i mappen {0}. (fejl {1})",

    // File open/save error string
    "ERROR_OPENING_FILE_TITLE"          : "Fejl ved åbning af fil",
    "ERROR_OPENING_FILE"                : "Der skete en fejl i forsøget på at åbne filen {0}. {1}",
    "ERROR_OPENING_FILES"               : "Der skete en fejl i forsøget på at åbne følgende filer:",
    "ERROR_RELOADING_FILE_TITLE"        : "Fejl ved genindlæsning af ændringer fra disk",
    "ERROR_RELOADING_FILE"              : "Der skete en fejl i forsøget på at genindlæse filen {0}. {1}",
    "ERROR_SAVING_FILE_TITLE"           : "Fejl ved gemning af fil",
    "ERROR_SAVING_FILE"                 : "Der skete en fejl i forsøget på at gemme filen {0}. {1}",
    "ERROR_RENAMING_FILE_TITLE"         : "Fejl ved omdøbning af fil",
    "ERROR_RENAMING_FILE"               : "Der skete en fejl i forsøget på at omdøbe filen {0}. {1}",
    "ERROR_DELETING_FILE_TITLE"         : "Fejl ved sletning af fil",
    "ERROR_DELETING_FILE"               : "Der skete en fejl i forsøget på at slette filen {0}. {1}",
    "INVALID_FILENAME_TITLE"            : "Ugyldigt {0}",
    "INVALID_FILENAME_MESSAGE"          : "{0} må ikke gøre brug af system-reserverede ord, ende med punktum (.) eller indeholde nogen af følgende tegn: {1}",
    "ENTRY_WITH_SAME_NAME_EXISTS"       : "En fil eller mappe med navnet {0} eksisterer allerede.",
    "ERROR_CREATING_FILE_TITLE"         : "Fejl ved oprettelse af {0}",
    "ERROR_CREATING_FILE"               : "Der skete en fejl i forsøget på at oprette {0} {1}. {2}",

    // Application preferences corrupt error strings
    "ERROR_PREFS_CORRUPT_TITLE"         : "Fejl ved indlæsning af indstillinger",
    "ERROR_PREFS_CORRUPT"               : "Din konfigurationsfil er ikke gyldig JSON. Filen vil blive åbnet, så du kan rette formateringen. Du bliver nødt til at genindlæse {APP_NAME} for at anvende ændringerne.",

    // Application error strings
    "ERROR_IN_BROWSER_TITLE"            : "Ups! {APP_NAME} virker ikke i browsere endnu.",
    "ERROR_IN_BROWSER"                  : "{APP_NAME} er lavet i HTML, men lige nu fungerer det som et almindeligt computerprogram, så du kan bruge det til at redigere lokale filer. Brug venligst programskallen fra github.com/adobe/brackets-shell til at køre {APP_NAME}.",

    // ProjectManager max files error string
    "ERROR_MAX_FILES_TITLE"             : "Fejl ved indeksering af filer",
    "ERROR_MAX_FILES"                   : "Det maksimale antal filer er blevet indekseret. Handlinger som tilgår filindekset fungerer måske ikke korrekt.",

    // Live Preview error strings
    "ERROR_LAUNCHING_BROWSER_TITLE"     : "Fejl ved start af browser",
    "ERROR_CANT_FIND_CHROME"            : "Google Chrome browseren kunne ikke findes. Undersøg venligst om det er installeret korrekt.",
    "ERROR_LAUNCHING_BROWSER"           : "Der skete en fejl ved start af browseren. (fejl {0})",

    "LIVE_DEVELOPMENT_ERROR_TITLE"      : "Fejl ved Live-Forhåndsvisning",
    "LIVE_DEVELOPMENT_RELAUNCH_TITLE"   : "Forbinder til browser",
    "LIVE_DEVELOPMENT_ERROR_MESSAGE"    : "For at Live-Forhåndsvisning kan forbinde, bliver Chrome nødt til at blive genstartet med fjern-fejlsøgning slået til.

Ønsker du at genstarte Chrome med fjern-fejlsøgning slået til?",
    "LIVE_DEV_LOADING_ERROR_MESSAGE"    : "Kan ikke indlæse siden for Live-Forhåndsvisning",
    "LIVE_DEV_NEED_HTML_MESSAGE"        : "Åbn en HTML-fil, eller sørg for at der er en fil ved navn index.html i dit projekt, før du starter Live-Forhåndsvisning.",
    "LIVE_DEV_NEED_BASEURL_MESSAGE"     : "For at bruge Live-Forhåndsvisning på en server-side fil, bliver du nødt til at angive en basis-URL for dette projekt.",
    "LIVE_DEV_SERVER_NOT_READY_MESSAGE" : "Fejl ved opstart af HTTP-serveren for Live-Forhåndsvisning. Prøv igen.",
    "LIVE_DEVELOPMENT_INFO_TITLE"       : "Velkommen til Live-Forhåndsvisning!",
    "LIVE_DEVELOPMENT_INFO_MESSAGE"     : "Live-Forhåndsvisning forbinder {APP_NAME} til din browser. Der åbnes en forhåndsvisning af din HTML-fil i browseren, som opdateres samtidig med at du redigerer i koden.

I denne tidlige version af {APP_NAME}, virker Live-Forhåndsvisning kun med Google Chrome og opdaterer direkte imens du redigerer i CSS- eller HTML-filer. Ændringer i JavaScript-filer bliver automatisk genindlæst når du gemmer.

(Denne besked bliver ikke vist igen.)",
    "LIVE_DEVELOPMENT_TROUBLESHOOTING"  : "For yderligere information, se Løsningsforslag til forbindelsesfejl vedrørende Live-Forhåndsvisning.",

    "LIVE_DEV_STATUS_TIP_NOT_CONNECTED" : "Live-Forhåndsvisning",
    "LIVE_DEV_STATUS_TIP_PROGRESS1"     : "Live-Forhåndsvisning: Forbinder\u2026",
    "LIVE_DEV_STATUS_TIP_PROGRESS2"     : "Live-Forhåndsvisning: Starter op\u2026",
    "LIVE_DEV_STATUS_TIP_CONNECTED"     : "Afbryd Live-Forhåndsvisning",
    "LIVE_DEV_STATUS_TIP_OUT_OF_SYNC"   : "Live-Forhåndsvisning (gem fil for at genindlæse)",
    "LIVE_DEV_STATUS_TIP_SYNC_ERROR"    : "Live-Forhåndsvisning (opdatering standset grundet syntaksfejl)",

    "LIVE_DEV_DETACHED_REPLACED_WITH_DEVTOOLS" : "Live-Forhåndsvisning blev afbrudt fordi udviklerværktøjer blev åbnet i browseren",
    "LIVE_DEV_DETACHED_TARGET_CLOSED"          : "Live-Forhåndsvisning blev afbrudt fordi siden blev lukket i browseren",
    "LIVE_DEV_NAVIGATED_AWAY"                  : "Live-Forhåndsvisning blev afbrudt fordi browseren navigerede til en side udenfor projektet",
    "LIVE_DEV_CLOSED_UNKNOWN_REASON"           : "Live-Forhåndsvisning blev afbrudt af ukendte årsager ({0})",

    "SAVE_CLOSE_TITLE"                  : "Gem ændringer",
    "SAVE_CLOSE_MESSAGE"                : "Ønsker du at gemme ændringerne i dokumentet {0}?",
    "SAVE_CLOSE_MULTI_MESSAGE"          : "Ønsker du at gemme ændringerne i følgende filer?",
    "EXT_MODIFIED_TITLE"                : "Eksterne ændringer",
    "CONFIRM_FOLDER_DELETE_TITLE"       : "Bekræft sletning",
    "CONFIRM_FOLDER_DELETE"             : "Er du sikker på at du vil slette mappen {0}?",
    "FILE_DELETED_TITLE"                : "Fil slettet",
    "EXT_MODIFIED_WARNING"              : "{0} er blevet ændret på disken.

Ønsker du at gemme filen og overskrive disse ændringer?",
    "EXT_MODIFIED_MESSAGE"              : "{0} er blevet ændret på disken, men har også ikke-gemte ændringer i {APP_NAME}.

Hvilken version ønsker du at beholde?",
    "EXT_DELETED_MESSAGE"               : "{0} er blevet slettet på disken, men har ikke-gemte ændringer i {APP_NAME}.

Ønsker du at beholde disse ændringer?",

    // Generic dialog/button labels
    "DONE"                              : "Færdig",
    "OK"                                : "OK",
    "CANCEL"                            : "Annuller",
    "DONT_SAVE"                         : "Gem ikke",
    "SAVE"                              : "Gem",
    "SAVE_AS"                           : "Gem som\u2026",
    "SAVE_AND_OVERWRITE"                : "Overskriv",
    "DELETE"                            : "Slet",
    "BUTTON_YES"                        : "Ja",
    "BUTTON_NO"                         : "Nej",

    // Find, Replace, Find in Files
    "FIND_RESULT_COUNT"                 : "{0} resultater",
    "FIND_RESULT_COUNT_SINGLE"          : "1 resultat",
    "FIND_NO_RESULTS"                   : "Ingen resultater",
    "FIND_QUERY_PLACEHOLDER"            : "Søg\u2026",
    "REPLACE_PLACEHOLDER"               : "Erstat med\u2026",
    "BUTTON_REPLACE_ALL"                : "Alle\u2026",
    "BUTTON_REPLACE_ALL_IN_FILES"       : "Erstat\u2026",
    "BUTTON_REPLACE"                    : "Erstat",
    "BUTTON_NEXT"                       : "\u25B6",
    "BUTTON_PREV"                       : "\u25C0",
    "BUTTON_NEXT_HINT"                  : "Næste forekomst",
    "BUTTON_PREV_HINT"                  : "Forrige forekomst",
    "BUTTON_CASESENSITIVE_HINT"         : "Match store og små bogstaver",
    "BUTTON_REGEXP_HINT"                : "Regulært udtryk",
    "REPLACE_WITHOUT_UNDO_WARNING_TITLE": "Erstat uden fortrydelse",
    "REPLACE_WITHOUT_UNDO_WARNING"      : "Da mere end {0} filer skal ændres, vil {APP_NAME} ændre uåbnede filer på disk.
Du vil ikke kunne fortryde ændringerne i disse filer.",
    "BUTTON_REPLACE_WITHOUT_UNDO"       : "Erstat uden fortrydelse",

    "OPEN_FILE"                         : "Åbn fil",
    "SAVE_FILE_AS"                      : "Gem fil",
    "CHOOSE_FOLDER"                     : "Vælg en mappe",

    "RELEASE_NOTES"                     : "Udgivelsesnoter",
    "NO_UPDATE_TITLE"                   : "Du er fuldt opdateret!",
    "NO_UPDATE_MESSAGE"                 : "Du bruger den seneste version af {APP_NAME}.",

    // Find and Replace
    "FIND_REPLACE_TITLE_LABEL"          : "Erstat",
    "FIND_REPLACE_TITLE_WITH"           : "med",
    "FIND_TITLE_LABEL"                  : "Fundet",
    "FIND_TITLE_SUMMARY"                : "— {0} {1} {2} i {3}",

    // Find in Files
    "FIND_NUM_FILES"                    : "{0} {1}",
    "FIND_IN_FILES_SCOPED"              : "i {0}",
    "FIND_IN_FILES_NO_SCOPE"            : "i projektet",
    "FIND_IN_FILES_ZERO_FILES"          : "Filtret udelukker alle filer {0}",
    "FIND_IN_FILES_FILE"                : "fil",
    "FIND_IN_FILES_FILES"               : "filer",
    "FIND_IN_FILES_MATCH"               : "forekomst",
    "FIND_IN_FILES_MATCHES"             : "forekomster",
    "FIND_IN_FILES_MORE_THAN"           : "Mere end ",
    "FIND_IN_FILES_PAGING"              : "{0}—{1}",
    "FIND_IN_FILES_FILE_PATH"           : "{0} {2} {1}", // We should use normal dashes on Windows instead of em dash eventually
    "FIND_IN_FILES_EXPAND_COLLAPSE"     : "Ctrl-/Cmd-klik for at udfolde/sammenfolde alt",
    "REPLACE_IN_FILES_ERRORS_TITLE"     : "Fejl ved erstatning",
    "REPLACE_IN_FILES_ERRORS"           : "Følgende filer blev ikke behandlet, da de har ændret sig siden søgningen eller ikke kunne gemmes.",

    "ERROR_FETCHING_UPDATE_INFO_TITLE"  : "Fejl ved hentning af opdateringsinformation",
    "ERROR_FETCHING_UPDATE_INFO_MSG"    : "Der opstod en fejl ved hentning af seneste opdateringsinformationer fra serveren. Sørg venligst for at du er forbundet til internettet og prøv igen.",

    // File exclusion filters
    "NEW_FILE_FILTER"                   : "Nyt udelukkelsessæt\u2026",
    "CLEAR_FILE_FILTER"                 : "Udeluk ingen filer",
    "NO_FILE_FILTER"                    : "Ingen filer udelukket",
    "EXCLUDE_FILE_FILTER"               : "Udeluk {0}",
    "EDIT_FILE_FILTER"                  : "Redigér\u2026",
    "FILE_FILTER_DIALOG"                : "Redigér udelukkelsessæt",
    "FILE_FILTER_INSTRUCTIONS"          : "Udeluk filer og mapper der matcher en eller flere af følgende (del)strenge eller jokertegn. Indtast hver streng på hver sin linje.",
    "FILTER_NAME_PLACEHOLDER"           : "Navngiv dette udelukkelsessæt (valgfrit)",
    "FILE_FILTER_CLIPPED_SUFFIX"        : "og {0} flere",
    "FILTER_COUNTING_FILES"             : "Tæller filer\u2026",
    "FILTER_FILE_COUNT"                 : "Tillader {0} af {1} filer {2}",
    "FILTER_FILE_COUNT_ALL"             : "Tillader alle {0} filer {1}",

    // Quick Edit
    "ERROR_QUICK_EDIT_PROVIDER_NOT_FOUND"   : "Ingen Lyn-Redigering tilgængelig for denne markørposition",
    "ERROR_CSSQUICKEDIT_BETWEENCLASSES"     : "CSS-Lyn-Redigering: placér markøren på et enkelt klassenavn",
    "ERROR_CSSQUICKEDIT_CLASSNOTFOUND"      : "CSS-Lyn-Redigering: ukomplet klasse-attribut",
    "ERROR_CSSQUICKEDIT_IDNOTFOUND"         : "CSS-Lyn-Redigering: ukomplet id-attribut",
    "ERROR_CSSQUICKEDIT_UNSUPPORTEDATTR"    : "CSS-Lyn-Redigering: placér markøren i et tag, en klasse eller et id",
    "ERROR_TIMINGQUICKEDIT_INVALIDSYNTAX"   : "CSS-Timing-Funktion Lyn-Redigering: ugyldig syntaks",
    "ERROR_JSQUICKEDIT_FUNCTIONNOTFOUND"    : "JS-Lyn-Redigering: placér markøren i et funktionsnavn",

    // Quick Docs
    "ERROR_QUICK_DOCS_PROVIDER_NOT_FOUND"   : "Ingen Lyn-Dokumentation tilgængelig for denne markørposition",

    /**
     * ProjectManager
     */
    "PROJECT_LOADING"   : "Indlæser\u2026",
    "UNTITLED"          : "Unavngivet",
    "WORKING_FILES"     : "Arbejdsfiler",

    /**
     * Keyboard modifier names
     */
    "KEYBOARD_CTRL"   : "Ctrl",
    "KEYBOARD_SHIFT"  : "Skift",
    "KEYBOARD_SPACE"  : "Mellemrum",

    /**
     * StatusBar strings
     */
    "STATUSBAR_CURSOR_POSITION"             : "Linje {0}, kolonne {1}",
    "STATUSBAR_SELECTION_CH_SINGULAR"       : " \u2014 {0} kolonne markeret",
    "STATUSBAR_SELECTION_CH_PLURAL"         : " \u2014 {0} kolonner markeret",
    "STATUSBAR_SELECTION_LINE_SINGULAR"     : " \u2014 {0} linje markeret",
    "STATUSBAR_SELECTION_LINE_PLURAL"       : " \u2014 {0} linjer markeret",
    "STATUSBAR_SELECTION_MULTIPLE"          : " \u2014 {0} markeringer",
    "STATUSBAR_INDENT_TOOLTIP_SPACES"       : "Klik for at skifte indrykning til mellemrum",
    "STATUSBAR_INDENT_TOOLTIP_TABS"         : "Klik for at skifte indrykning til tabulatorstop",
    "STATUSBAR_INDENT_SIZE_TOOLTIP_SPACES"  : "Klik for at ændre antallet af mellemrum ved indrykning",
    "STATUSBAR_INDENT_SIZE_TOOLTIP_TABS"    : "Klik for at ændre bredde af tabulatorstop",
    "STATUSBAR_SPACES"                      : "Mellemrum:",
    "STATUSBAR_TAB_SIZE"                    : "Tabulatorbredde:",
    "STATUSBAR_LINE_COUNT_SINGULAR"         : "\u2014 {0} linje",
    "STATUSBAR_LINE_COUNT_PLURAL"           : "\u2014 {0} linjer",
    "STATUSBAR_USER_EXTENSIONS_DISABLED"    : "Udvidelser slået fra",
    "STATUSBAR_INSERT"                      : "INS",
    "STATUSBAR_OVERWRITE"                   : "OVR",

    // CodeInspection: errors/warnings
    "ERRORS_PANEL_TITLE_MULTIPLE"           : "{0} problemer",
    "SINGLE_ERROR"                          : "1 {0}-problem",
    "MULTIPLE_ERRORS"                       : "{1} {0}-problemer",
    "NO_ERRORS"                             : "Ingen {0}-problemer fundet - godt gået!",
    "NO_ERRORS_MULTIPLE_PROVIDER"           : "Ingen problemer fundet - godt gået!",
    "LINT_DISABLED"                         : "Lintning er slået fra",
    "NO_LINT_AVAILABLE"                     : "Ingen linter tilgængelig for {0}",
    "NOTHING_TO_LINT"                       : "Intet at linte",
    "LINTER_TIMED_OUT"                      : "{0} har opgivet at vente efter {1} ms",
    "LINTER_FAILED"                         : "{0} afsluttede med fejl: {1}",

    /**
     * Command Name Constants
     */

    // File menu commands
    "FILE_MENU"                           : "Filer",
    "CMD_FILE_NEW_UNTITLED"               : "Ny",
    "CMD_FILE_NEW"                        : "Ny fil",
    "CMD_FILE_NEW_FOLDER"                 : "Ny mappe",
    "CMD_FILE_OPEN"                       : "Åbn\u2026",
    "CMD_ADD_TO_WORKING_SET"              : "Tilføj til arbejdsfiler",
    "CMD_OPEN_DROPPED_FILES"              : "Åbn droppede filer",
    "CMD_OPEN_FOLDER"                     : "Åbn mappe\u2026",
    "CMD_FILE_CLOSE"                      : "Luk",
    "CMD_FILE_CLOSE_ALL"                  : "Luk alle",
    "CMD_FILE_CLOSE_LIST"                 : "Luk liste",
    "CMD_FILE_CLOSE_OTHERS"               : "Luk øvrige",
    "CMD_FILE_CLOSE_ABOVE"                : "Luk ovenstående",
    "CMD_FILE_CLOSE_BELOW"                : "Luk nedenstående",
    "CMD_FILE_SAVE"                       : "Gem",
    "CMD_FILE_SAVE_ALL"                   : "Gem alle",
    "CMD_FILE_SAVE_AS"                    : "Gem som\u2026",
    "CMD_LIVE_FILE_PREVIEW"               : "Live-Forhåndsvisning",
    "CMD_PROJECT_SETTINGS"                : "Projektindstillinger\u2026",
    "CMD_FILE_RENAME"                     : "Omdøb",
    "CMD_FILE_DELETE"                     : "Slet",
    "CMD_INSTALL_EXTENSION"               : "Installér udvidelse\u2026",
    "CMD_EXTENSION_MANAGER"               : "Udvidelseshåndtering\u2026",
    "CMD_FILE_REFRESH"                    : "Genindlæs filtræ",
    "CMD_QUIT"                            : "Afslut",
    // Used in native File menu on Windows
    "CMD_EXIT"                            : "Afslut",

    // Edit menu commands
    "EDIT_MENU"                           : "Redigér",
    "CMD_UNDO"                            : "Fortryd",
    "CMD_REDO"                            : "Gentag",
    "CMD_CUT"                             : "Klip",
    "CMD_COPY"                            : "Kopiér",
    "CMD_PASTE"                           : "Sæt ind",
    "CMD_SELECT_ALL"                      : "Markér alt",
    "CMD_SELECT_LINE"                     : "Markér linje",
    "CMD_SPLIT_SEL_INTO_LINES"            : "Opdel markering i linjer",
    "CMD_ADD_CUR_TO_NEXT_LINE"            : "Tilføj markør til næste linje",
    "CMD_ADD_CUR_TO_PREV_LINE"            : "Tilføj markør til forrige linje",
    "CMD_INDENT"                          : "Indryk",
    "CMD_UNINDENT"                        : "Udryk",
    "CMD_DUPLICATE"                       : "Duplikér",
    "CMD_DELETE_LINES"                    : "Slet linje",
    "CMD_COMMENT"                         : "Slå linje-kommentar til/fra",
    "CMD_BLOCK_COMMENT"                   : "Slå blok-kommentar til/fra",
    "CMD_LINE_UP"                         : "Flyt linje op",
    "CMD_LINE_DOWN"                       : "Flyt linje ned",
    "CMD_OPEN_LINE_ABOVE"                 : "Åbn linjen ovenover",
    "CMD_OPEN_LINE_BELOW"                 : "Åbn linjen nedenunder",
    "CMD_TOGGLE_CLOSE_BRACKETS"           : "Luk parenteser automatisk",
    "CMD_SHOW_CODE_HINTS"                 : "Vis kodehjælp",

    // Search menu commands
    "FIND_MENU"                           : "Søg",
    "CMD_FIND"                            : "Søg",
    "CMD_FIND_NEXT"                       : "Find næste",
    "CMD_FIND_PREVIOUS"                   : "Find forrige",
    "CMD_FIND_ALL_AND_SELECT"             : "Find alle og markér",
    "CMD_ADD_NEXT_MATCH"                  : "Tilføj næste forekomst til markeringer",
    "CMD_SKIP_CURRENT_MATCH"              : "Spring over og tilføj næste forekomst",
    "CMD_FIND_IN_FILES"                   : "Søg i filer",
    "CMD_FIND_IN_SELECTED"                : "Søg i valgte fil/mappe",
    "CMD_FIND_IN_SUBTREE"                 : "Søg i\u2026",
    "CMD_REPLACE"                         : "Erstat",
    "CMD_REPLACE_IN_FILES"                : "Erstat i filer",
    "CMD_REPLACE_IN_SELECTED"             : "Erstat i valgte fil/mappe",
    "CMD_REPLACE_IN_SUBTREE"              : "Erstat i\u2026",

    // View menu commands
    "VIEW_MENU"                           : "Vis",
    "CMD_HIDE_SIDEBAR"                    : "Skjul sidepanel",
    "CMD_SHOW_SIDEBAR"                    : "Vis sidepanel",
    "CMD_INCREASE_FONT_SIZE"              : "Forøg skriftstørrelse",
    "CMD_DECREASE_FONT_SIZE"              : "Formindsk skriftstørrelse",
    "CMD_RESTORE_FONT_SIZE"               : "Nulstil skriftstørrelse",
    "CMD_SCROLL_LINE_UP"                  : "Rul en linje op",
    "CMD_SCROLL_LINE_DOWN"                : "Rul en linje ned",
    "CMD_TOGGLE_LINE_NUMBERS"             : "Linjenumre",
    "CMD_TOGGLE_ACTIVE_LINE"              : "Fremhæv aktiv linje",
    "CMD_TOGGLE_WORD_WRAP"                : "Tekstombrydning",
    "CMD_LIVE_HIGHLIGHT"                  : "Fremhæv i Live-Forhåndsvisning",
    "CMD_VIEW_TOGGLE_INSPECTION"          : "Lint filer når de gemmes",
    "CMD_SORT_WORKINGSET_BY_ADDED"        : "Sortér efter tilføjelse",
    "CMD_SORT_WORKINGSET_BY_NAME"         : "Sortér efter navn",
    "CMD_SORT_WORKINGSET_BY_TYPE"         : "Sortér efter type",
    "CMD_SORT_WORKINGSET_AUTO"            : "Automatisk sortering",

    // Navigate menu Commands
    "NAVIGATE_MENU"                       : "Navigér",
    "CMD_QUICK_OPEN"                      : "Lyn-åbn",
    "CMD_GOTO_LINE"                       : "Gå til linje",
    "CMD_GOTO_DEFINITION"                 : "Lyn-søg efter definition",
    "CMD_GOTO_FIRST_PROBLEM"              : "Gå til første fejl/advarsel",
    "CMD_TOGGLE_QUICK_EDIT"               : "Lyn-Redigering",
    "CMD_TOGGLE_QUICK_DOCS"               : "Lyn-Dokumentation",
    "CMD_QUICK_EDIT_PREV_MATCH"           : "Forrige forekomst",
    "CMD_QUICK_EDIT_NEXT_MATCH"           : "Næste forekomst",
    "CMD_CSS_QUICK_EDIT_NEW_RULE"         : "Ny regel",
    "CMD_NEXT_DOC"                        : "Næste dokument",
    "CMD_PREV_DOC"                        : "Forrige dokument",
    "CMD_SHOW_IN_TREE"                    : "Vis i filtræ",
    "CMD_SHOW_IN_EXPLORER"                : "Vis i Stifinder",
    "CMD_SHOW_IN_FINDER"                  : "Vis i Finder",
    "CMD_SHOW_IN_OS"                      : "Vis i OS",

    // Help menu commands
    "HELP_MENU"                           : "Hjælp",
    "CMD_CHECK_FOR_UPDATE"                : "Kontrollér for opdateringer",
    "CMD_HOW_TO_USE_BRACKETS"             : "Sådan bruger du {APP_NAME}",
    "CMD_SUPPORT"                         : "{APP_NAME} support",
    "CMD_SUGGEST"                         : "Foreslå en funktion",
    "CMD_RELEASE_NOTES"                   : "Udgivelsesnoter",
    "CMD_GET_INVOLVED"                    : "Bliv involveret",
    "CMD_SHOW_EXTENSIONS_FOLDER"          : "Vis udvidelsesmappe",
    "CMD_HOMEPAGE"                        : "{APP_TITLE} hjemmeside",
    "CMD_TWITTER"                         : "{TWITTER_NAME} på Twitter",
    "CMD_ABOUT"                           : "Om {APP_TITLE}",
    "CMD_OPEN_PREFERENCES"                : "Åbn konfigurationsfil",

    // Strings for main-view.html
    "EXPERIMENTAL_BUILD"                   : "eksperimentiel version",
    "DEVELOPMENT_BUILD"                    : "udviklerversion",
    "RELOAD_FROM_DISK"                     : "Genindlæs fra disk",
    "KEEP_CHANGES_IN_EDITOR"               : "Behold ændringer i editor",
    "CLOSE_DONT_SAVE"                      : "Luk (uden at gemme)",
    "RELAUNCH_CHROME"                      : "Genstart Chrome",
    "ABOUT"                                : "Om",
    "CLOSE"                                : "Luk",
    "ABOUT_TEXT_LINE1"                     : "sprint {VERSION_MINOR} {BUILD_TYPE} {VERSION}",
    "ABOUT_TEXT_BUILD_TIMESTAMP"           : "fabrikationstidspunkt: ",
    "ABOUT_TEXT_LINE3"                     : "Bemærkninger om og/eller yderligere betingelser og vilkår for tredjeparts software kan findes på {ADOBE_THIRD_PARTY} og medtaget heri som reference.",
    "ABOUT_TEXT_LINE4"                     : "Dokumentation og kildekode på https://github.com/adobe/brackets/",
    "ABOUT_TEXT_LINE5"                     : "Skabt med \u2764 og JavaScript af:",
    "ABOUT_TEXT_LINE6"                     : "En masse mennesker (men vi har problemer med at hente det data i øjeblikket).",
    "ABOUT_TEXT_WEB_PLATFORM_DOCS"         : "Dokumentationen og det grafiske logo fra Web Platform er udgivet under en Creative Commons Attribution licens, CC-BY 3.0 Unported.",
    "UPDATE_NOTIFICATION_TOOLTIP"          : "Der ligger en ny version af {APP_NAME} klar! Klik her for at læse mere.",
    "UPDATE_AVAILABLE_TITLE"               : "Opdatering klar",
    "UPDATE_MESSAGE"                       : "Hey! Der er en ny version af {APP_NAME} tilgængelig. Her er nogen af de nye funktioner:",
    "GET_IT_NOW"                           : "Hent den nu!",
    "PROJECT_SETTINGS_TITLE"               : "Projektindstillinger for: {0}",
    "PROJECT_SETTING_BASE_URL"             : "Basis-URL",
    "PROJECT_SETTING_BASE_URL_HINT"        : "For at bruge en lokal server, indtast en URL såsom http://localhost:8000/",
    "BASEURL_ERROR_INVALID_PROTOCOL"       : "{0}-protokollen er ikke understøttet af Live-Forhåndsvisning—Brug venligst http: eller https: .",
    "BASEURL_ERROR_SEARCH_DISALLOWED"      : "Basis-URL'en kan ikke indeholde søgeparametre såsom \"{0}\".",
    "BASEURL_ERROR_HASH_DISALLOWED"        : "Basis-URL'en kan ikke indeholde interne henvisninger såsom \"{0}\".",
    "BASEURL_ERROR_INVALID_CHAR"           : "Specielle tegn såsom '{0}' skal kodes med %.",
    "BASEURL_ERROR_UNKNOWN_ERROR"          : "Ukendt fejl ved tolkning af Basis-URL",

    // CSS Quick Edit
    "BUTTON_NEW_RULE"                      : "Ny regel",

    // Extension Management strings
    "INSTALL"                              : "Installér",
    "UPDATE"                               : "Opdatér",
    "REMOVE"                               : "Fjern",
    "OVERWRITE"                            : "Overskriv",
    "CANT_REMOVE_DEV"                      : "Udvidelser i \"dev\"-mappen skal slettes manuelt.",
    "CANT_UPDATE"                          : "Opdateringen er ikke kompatibel med denne version af {APP_NAME}.",
    "CANT_UPDATE_DEV"                      : "Udvidelser i \"dev\"-mappen kan ikke opdateres automatisk.",
    "INSTALL_EXTENSION_TITLE"              : "Installér udvidelse",
    "UPDATE_EXTENSION_TITLE"               : "Opdatér udvidelse",
    "INSTALL_EXTENSION_LABEL"              : "Udvidelses-URL",
    "INSTALL_EXTENSION_HINT"               : "URL til udvidelsens zip-fil eller GitHub-repo",
    "INSTALLING_FROM"                      : "Installerer udvidelse fra {0}\u2026",
    "INSTALL_SUCCEEDED"                    : "Installation lykkedes!",
    "INSTALL_FAILED"                       : "Installation mislykkedes.",
    "CANCELING_INSTALL"                    : "Afbryder\u2026",
    "CANCELING_HUNG"                       : "Afbrydelsen af installationen tager lang tid. En intern fejl er måske opstået.",
    "INSTALL_CANCELED"                     : "Installation afbrudt.",
    // These must match the error codes in ExtensionsDomain.Errors.* :
    "INVALID_ZIP_FILE"                     : "Det modtagede indhold er ikke en gyldig zip-fil.",
    "INVALID_PACKAGE_JSON"                 : "Filen package.json er ugyldig (fejl: {0}).",
    "MISSING_PACKAGE_NAME"                 : "Filen package.json angiver ikke et pakkenavn.",
    "BAD_PACKAGE_NAME"                     : "{0} er et ugyldigt pakkenavn.",
    "MISSING_PACKAGE_VERSION"              : "Filen package.json angiver ikke en pakkeversion.",
    "INVALID_VERSION_NUMBER"               : "Pakkeversionsnummeret ({0}) er ugyldigt.",
    "INVALID_BRACKETS_VERSION"             : "Kompatibilitets-strengen for {APP_NAME} ({0}) er ugyldig.",
    "DISALLOWED_WORDS"                     : "Ordene ({1}) er ikke tilladt i {0}-feltet.",
    "API_NOT_COMPATIBLE"                   : "Udvidelsen er ikke kompatibel med denne version af {APP_NAME}. Den er installeret i mappen for deaktiverede udvidelser.",
    "MISSING_MAIN"                         : "Pakken har ingen fil ved navn main.js.",
    "EXTENSION_ALREADY_INSTALLED"          : "Denne pakke vil overskrive en tidligere installation. Overskriv den gamle udvidelse?",
    "EXTENSION_SAME_VERSION"               : "Denne pakke er samme version som den installerede. Overskriv den eksisterende installation?",
    "EXTENSION_OLDER_VERSION"              : "Denne pakke er version {0}, som er ældre end den nuværende installation ({1}). Overskriv den eksisterende installation?",
    "DOWNLOAD_ID_IN_USE"                   : "Intern fejl: download-ID er allerede i brug.",
    "NO_SERVER_RESPONSE"                   : "Kan ikke forbinde til server.",
    "BAD_HTTP_STATUS"                      : "Filen kunne ikke findes på serveren (HTTP {0}).",
    "CANNOT_WRITE_TEMP"                    : "Ikke i stand til at gemme nedhentede data til en midlertidig fil.",
    "ERROR_LOADING"                        : "Udvidelsen stødte på en fejl imens den startede op.",
    "MALFORMED_URL"                        : "URL'en er ugyldig. Sørg for at du har indtastet den korrekt.",
    "UNSUPPORTED_PROTOCOL"                 : "URL'en skal være en http- eller https-URL.",
    "UNKNOWN_ERROR"                        : "Ukendt intern fejl.",
    // For NOT_FOUND_ERR, see generic strings above
    "EXTENSION_MANAGER_TITLE"              : "Udvidelseshåndtering",
    "EXTENSION_MANAGER_ERROR_LOAD"         : "Ikke i stand til at tilgå udvidelsesregistret. Prøv igen senere.",
    "INSTALL_FROM_URL"                     : "Installér fra URL\u2026",
    "EXTENSION_AUTHOR"                     : "Forfatter",
    "EXTENSION_DATE"                       : "Dato",
    "EXTENSION_INCOMPATIBLE_NEWER"         : "Denne udvidelse kræver en nyere version af {APP_NAME}.",
    "EXTENSION_INCOMPATIBLE_OLDER"         : "Denne udvidelse virker i øjeblikket kun med ældre versioner af {APP_NAME}.",
    "EXTENSION_LATEST_INCOMPATIBLE_NEWER"  : "Version {0} af denne udvidelse kræver en nyere version af {APP_NAME}. Men du kan installere den tidligere version {1}.",
    "EXTENSION_LATEST_INCOMPATIBLE_OLDER"  : "Version {0} af denne udvidelse virker kun med en ældre version af {APP_NAME}. Men du kan installere den tidligere version {1}.",
    "EXTENSION_NO_DESCRIPTION"             : "Ingen beskrivelse",
    "EXTENSION_MORE_INFO"                  : "Mere information...",
    "EXTENSION_ERROR"                      : "Udvidelsesfejl",
    "EXTENSION_KEYWORDS"                   : "Nøgleord",
    "EXTENSION_INSTALLED"                  : "Installeret",
    "EXTENSION_UPDATE_INSTALLED"           : "Opdatering til denne udvidelse er blevet hentet og vil blive installeret efter at {APP_NAME} er genindlæst.",
    "EXTENSION_SEARCH_PLACEHOLDER"         : "Søg",
    "EXTENSION_MORE_INFO_LINK"             : "Mere",
    "BROWSE_EXTENSIONS"                    : "Gennemse udvidelser",
    "EXTENSION_MANAGER_REMOVE"             : "Fjern udvidelse",
    "EXTENSION_MANAGER_REMOVE_ERROR"       : "Ikke i stand til at fjerne en eller flere udvidelser: {0}. {APP_NAME} vil stadig blive genindlæst.",
    "EXTENSION_MANAGER_UPDATE"             : "Opdatér udvidelse",
    "EXTENSION_MANAGER_UPDATE_ERROR"       : "Ikke i stand til at opdatere en eller flere udvidelser: {0}. {APP_NAME} vil stadig blive genindlæst.",
    "MARKED_FOR_REMOVAL"                   : "Markeret til fjernelse",
    "UNDO_REMOVE"                          : "Fortryd",
    "MARKED_FOR_UPDATE"                    : "Markeret til opdatering",
    "UNDO_UPDATE"                          : "Fortryd",
    "CHANGE_AND_RELOAD_TITLE"              : "Gennemfør ændringer på udvidelser",
    "CHANGE_AND_RELOAD_MESSAGE"            : "For at opdatere eller fjerne de valgte udvidelser bliver {APP_NAME} nødt til at blive genindlæst. Du vil blive bedt om at gemme ikke-gemte ændringer.",
    "REMOVE_AND_RELOAD"                    : "Fjern udvidelser og genindlæs",
    "CHANGE_AND_RELOAD"                    : "Gennemfør ændringer og genindlæs",
    "UPDATE_AND_RELOAD"                    : "Opdatér udvidelser og genindlæs",
    "PROCESSING_EXTENSIONS"                : "Behandler udvidelsesændringer\u2026",
    "EXTENSION_NOT_INSTALLED"              : "Kunne ikke fjerne udvidelsen {0} da den ikke er installeret.",
    "NO_EXTENSIONS"                        : "Ingen udvidelser er installeret endnu.
Klik på \"Udvalg\" foroven for at komme i gang.",
    "NO_EXTENSION_MATCHES"                 : "Ingen udvidelser matcher din søgning.",
    "REGISTRY_SANITY_CHECK_WARNING"        : "Vær forsigtig med at installere udvidelser fra en ukendt kilde.",
    "EXTENSIONS_INSTALLED_TITLE"           : "Installeret",
    "EXTENSIONS_AVAILABLE_TITLE"           : "Udvalg",
    "EXTENSIONS_UPDATES_TITLE"             : "Opdateringer",

    "INLINE_EDITOR_NO_MATCHES"             : "Ingen forekomster.",
    "CSS_QUICK_EDIT_NO_MATCHES"            : "Der eksisterer ingen CSS-regler, som matcher det markerede element.
 Klik \"Ny regel\" for at oprette en.",
    "CSS_QUICK_EDIT_NO_STYLESHEETS"        : "Der er ingen stylesheets i dit projekt.
Opret et for at tilføje CSS-regler.",

    // Custom Viewers
    "IMAGE_VIEWER_LARGEST_ICON"            : "største",

    /**
     * Unit names
     */

    "UNIT_PIXELS"                          : "pixler",

    // extensions/default/DebugCommands
    "DEBUG_MENU"                                : "Fejlsøgning",
    "ERRORS"                                    : "Fejl",
    "CMD_SHOW_DEV_TOOLS"                        : "Vis udviklerværktøjer",
    "CMD_REFRESH_WINDOW"                        : "Genindlæs med udvidelser",
    "CMD_RELOAD_WITHOUT_USER_EXTS"              : "Genindlæs uden udvidelser",
    "CMD_NEW_BRACKETS_WINDOW"                   : "Nyt {APP_NAME} vindue",
    "CMD_SWITCH_LANGUAGE"                       : "Skift sprog",
    "CMD_RUN_UNIT_TESTS"                        : "Kør enhedstest",
    "CMD_SHOW_PERF_DATA"                        : "Vis ydelsesdata",
    "CMD_ENABLE_NODE_DEBUGGER"                  : "Slå Node-debugger til",
    "CMD_LOG_NODE_STATE"                        : "Log Node tilstand til konsol",
    "CMD_RESTART_NODE"                          : "Genstart Node",
    "CMD_SHOW_ERRORS_IN_STATUS_BAR"             : "Vis fejl i statuslinjen",

    "LANGUAGE_TITLE"                            : "Skift sprog",
    "LANGUAGE_MESSAGE"                          : "Sprog:",
    "LANGUAGE_SUBMIT"                           : "Genindlæs {APP_NAME}",
    "LANGUAGE_CANCEL"                           : "Annuller",
    "LANGUAGE_SYSTEM_DEFAULT"                   : "Systemets sprog",

    // extensions/default/InlineTimingFunctionEditor
    "INLINE_TIMING_EDITOR_TIME"                 : "Tid",
    "INLINE_TIMING_EDITOR_PROGRESSION"          : "Forløb",
    "BEZIER_EDITOR_INFO"                        : "↑↓←→ Flyt valgte punkt
Skift Flyt ti enheder af gangen
Tab Skift punkter",
    "STEPS_EDITOR_INFO"                         : "↑↓ Forøg eller formindsk antal trin
←→ 'Start' eller 'End'",
    "INLINE_TIMING_EDITOR_INVALID"              : "Den gamle værdi {0} er ikke gyldig, så den viste funktion blev ændret til {1}. Dokumentet vil blive opdateret ved første redigering.",

    // extensions/default/InlineColorEditor
    "COLOR_EDITOR_CURRENT_COLOR_SWATCH_TIP"     : "Nuværende farve",
    "COLOR_EDITOR_ORIGINAL_COLOR_SWATCH_TIP"    : "Oprindelig farve",
    "COLOR_EDITOR_RGBA_BUTTON_TIP"              : "RGBa-format",
    "COLOR_EDITOR_HEX_BUTTON_TIP"               : "Hex-format",
    "COLOR_EDITOR_HSLA_BUTTON_TIP"              : "HSLa-format",
    "COLOR_EDITOR_USED_COLOR_TIP_SINGULAR"      : "{0} (Brugt {1} gang)",
    "COLOR_EDITOR_USED_COLOR_TIP_PLURAL"        : "{0} (Brugt {1} gange)",

    // extensions/default/JavaScriptCodeHints
    "CMD_JUMPTO_DEFINITION"                     : "Hop til definition",
    "CMD_SHOW_PARAMETER_HINT"                   : "Vis parameterhjælp",
    "NO_ARGUMENTS"                              : "<ingen parametre>",

    // extensions/default/JSLint
    "JSLINT_NAME"                               : "JSLint",

    // extensions/default/QuickView
    "CMD_ENABLE_QUICK_VIEW"                     : "Lyn-visning af det du peger på",

    // extensions/default/RecentProjects
    "CMD_TOGGLE_RECENT_PROJECTS"                : "Seneste projekter",

    // extensions/default/WebPlatformDocs
    "DOCS_MORE_LINK"                            : "Læs mere"
});

/* Last translated for 3a762c3cf91d6f65a5bb19aeb2056afacd777c71 */


Component.prototype.setState = function(update, callback) {

// only clone state when copying to nextState the first time.

(this._nextState !== this.state && this._nextState) ||

(this._nextState = assign({}, this.state));

keep keep keep replace replace replace keep

s = this._nextState = assign({}, this.state);

import { IS_NON_DIMENSIONAL } from '../constants';
import options from '../options';

/**
 * Diff the old and new properties of a VNode and apply changes to the DOM node
 * @param {import('../internal').PreactElement} dom The DOM node to apply
 * changes to
 * @param {object} newProps The new props
 * @param {object} oldProps The old props
 * @param {boolean} isSvg Whether or not this node is an SVG node
 * @param {boolean} hydrate Whether or not we are in hydration mode
 */
export function diffProps(dom, newProps, oldProps, isSvg, hydrate) {
	let i;

	for (i in oldProps) {
		if (!(i in newProps)) {
			setProperty(dom, i, null, oldProps[i], isSvg);
		}
	}

	for (i in newProps) {
		if (
			(!hydrate || typeof newProps[i] == 'function') &&
			i !== 'value' &&
			i !== 'checked' &&
			oldProps[i] !== newProps[i]
		) {
			setProperty(dom, i, newProps[i], oldProps[i], isSvg);
		}
	}
}

function setStyle(style, key, value) {
	if (key[0] === '-') {
		style.setProperty(key, value);
	} else {
		style[key] =
			typeof value === 'number' && IS_NON_DIMENSIONAL.test(key) === false
				? value + 'px'
				: value == null
				? ''
				: value;
	}
}

/**
 * Set a property value on a DOM node
 * @param {import('../internal').PreactElement} dom The DOM node to modify
 * @param {string} name The name of the property to set
 * @param {*} value The value to set the property to
 * @param {*} oldValue The old value the property had
 * @param {boolean} isSvg Whether or not this DOM node is an SVG node or not
 */
function setProperty(dom, name, value, oldValue, isSvg) {
	name = isSvg
		? name === 'className'
			? 'class'
			: name
		: name === 'class'
		? 'className'
		: name;

	if (name === 'key' || name === 'children') {
	} else if (name === 'style') {
		const s = dom.style;

		if (typeof value === 'string') {
			s.cssText = value;
		} else {
			if (typeof oldValue === 'string') {
				s.cssText = '';
				oldValue = null;
			}

			if (oldValue)
				for (let i in oldValue) {
					if (!(value && i in value)) {
						setStyle(s, i, '');
					}
				}

			if (value)
				for (let i in value) {
					if (!oldValue || value[i] !== oldValue[i]) {
						setStyle(s, i, value[i]);
					}
				}
		}
	}
	// Benchmark for comparison: https://esbench.com/bench/574c954bdb965b9a00965ac6
	else if (name[0] === 'o' && name[1] === 'n') {
		let useCapture = name !== (name = name.replace(/Capture$/, ''));
		let nameLower = name.toLowerCase();
		name = (nameLower in dom ? nameLower : name).slice(2);

		if (value) {
			if (!oldValue) dom.addEventListener(name, eventProxy, useCapture);
			(dom._listeners || (dom._listeners = {}))[name] = value;
		} else {
			dom.removeEventListener(name, eventProxy, useCapture);
		}
	} else if (
		name !== 'list' &&
		name !== 'tagName' &&
		// HTMLButtonElement.form and HTMLInputElement.form are read-only but can be set using
		// setAttribute
		name !== 'form' &&
		!isSvg &&
		name in dom
	) {
		dom[name] = value == null ? '' : value;
	} else if (
		typeof value !== 'function' &&
		name !== 'dangerouslySetInnerHTML'
	) {
		if (name !== (name = name.replace(/^xlink:?/, ''))) {
			if (value == null || value === false) {
				dom.removeAttributeNS(
					'http://www.w3.org/1999/xlink',
					name.toLowerCase()
				);
			} else {
				dom.setAttributeNS(
					'http://www.w3.org/1999/xlink',
					name.toLowerCase(),
					value
				);
			}
		} else if (value == null || value === false) {
			dom.removeAttribute(name);
		} else {
			dom.setAttribute(name, value);
		}
	}
}

/**
 * Proxy an event to hooked event handlers
 * @param {Event} e The event object from the browser
 * @private
 */
function eventProxy(e) {
	this._listeners[e.type](options.event ? options.event(e) : e);
}


import { setupRerender } from 'preact/test-utils';
import { createElement, render, Component } from '../../../src/index';
import { setupScratch, teardown } from '../../_util/helpers';

/** @jsx createElement */

describe('Lifecycle methods', () => {
	/** @type {HTMLDivElement} */
	let scratch;

	/** @type {() => void} */
	let rerender;

	beforeEach(() => {
		scratch = setupScratch();
		rerender = setupRerender();
	});

	afterEach(() => {
		teardown(scratch);
	});

	describe('#componentWillUpdate', () => {
		it('should NOT be called on initial render', () => {
			class ReceivePropsComponent extends Component {
				componentWillUpdate() {}
				render() {
					return <div />;
				}
			}
			sinon.spy(ReceivePropsComponent.prototype, 'componentWillUpdate');
			render(<ReceivePropsComponent />, scratch);
			expect(ReceivePropsComponent.prototype.componentWillUpdate).not.to.have
				.been.called;
		});

		it('should be called when rerender with new props from parent', () => {
			let doRender;
			class Outer extends Component {
				constructor(p, c) {
					super(p, c);
					this.state = { i: 0 };
				}
				componentDidMount() {
					doRender = () => this.setState({ i: this.state.i + 1 });
				}
				render(props, { i }) {
					return <Inner i={i} {...props} />;
				}
			}
			class Inner extends Component {
				componentWillUpdate(nextProps, nextState) {
					expect(nextProps).to.be.deep.equal({ i: 1 });
					expect(nextState).to.be.deep.equal({});
				}
				render() {
					return <div />;
				}
			}
			sinon.spy(Inner.prototype, 'componentWillUpdate');
			sinon.spy(Outer.prototype, 'componentDidMount');

			// Initial render
			render(<Outer />, scratch);
			expect(Inner.prototype.componentWillUpdate).not.to.have.been.called;

			// Rerender inner with new props
			doRender();
			rerender();
			expect(Inner.prototype.componentWillUpdate).to.have.been.called;
		});

		it('should be called on new state', () => {
			let doRender;
			class ReceivePropsComponent extends Component {
				componentWillUpdate() {}
				componentDidMount() {
					doRender = () => this.setState({ i: this.state.i + 1 });
				}
				render() {
					return <div />;
				}
			}
			sinon.spy(ReceivePropsComponent.prototype, 'componentWillUpdate');
			render(<ReceivePropsComponent />, scratch);
			expect(ReceivePropsComponent.prototype.componentWillUpdate).not.to.have
				.been.called;

			doRender();
			rerender();
			expect(ReceivePropsComponent.prototype.componentWillUpdate).to.have.been
				.called;
		});
	});
});


import React, {
	render,
	createElement,
	cloneElement,
	findDOMNode,
	Component,
	unmountComponentAtNode,
	createFactory,
	unstable_batchedUpdates
} from '../../src';
import { createElement as preactH } from 'preact';
import {
	setupScratch,
	teardown,
	createEvent
} from '../../../test/_util/helpers';

let ce = type => document.createElement(type);
let text = text => document.createTextNode(text);

describe('preact-compat', () => {
	/** @type {HTMLDivElement} */
	let scratch, proto;

	beforeEach(() => {
		scratch = setupScratch();
		proto = document.createElement('div').constructor.prototype;
		sinon.spy(proto, 'addEventListener');
		sinon.spy(proto, 'removeEventListener');
	});

	afterEach(() => {
		proto.addEventListener.restore();
		proto.removeEventListener.restore();
		teardown(scratch);
	});

	describe('render()', () => {
		it('should be exported', () => {
			expect(React)
				.to.have.property('render')
				.that.is.a('function')
				.that.equals(render);
		});

		it('should replace isomorphic content', () => {
			let root = ce('div');
			let initialChild = ce('div');
			initialChild.appendChild(text('initial content'));
			root.appendChild(initialChild);

			render(
dynamic content
, root);
			expect(root)
				.to.have.property('textContent')
				.that.is.a('string')
				.that.equals('dynamic content');
		});

		it('should remove extra elements', () => {
			let root = ce('div');
			let inner = ce('div');

			root.appendChild(inner);

			let c1 = ce('div');
			c1.appendChild(text('isomorphic content'));
			inner.appendChild(c1);

			let c2 = ce('div');
			c2.appendChild(text('extra content'));
			inner.appendChild(c2);

			render(
dynamic content
, root);
			expect(root)
				.to.have.property('textContent')
				.that.is.a('string')
				.that.equals('dynamic content');
		});

		// Note: Replacing text nodes inside the root itself is currently unsupported.
		// We do replace them everywhere else, though.
		it('should remove text nodes', () => {
			let root = ce('div');

			let div = ce('div');
			root.appendChild(div);
			div.appendChild(text('Text Content'));
			div.appendChild(text('More Text Content'));

			render(
dynamic content
, root);
			expect(root)
				.to.have.property('textContent')
				.that.is.a('string')
				.that.equals('dynamic content');
		});

		it('should support onAnimationEnd', () => {
			const func = () => {};
			render(
, scratch);

			expect(
				proto.addEventListener
			).to.have.been.calledOnce.and.to.have.been.calledWithExactly(
				'animationend',
				sinon.match.func,
				false
			);

			expect(scratch.firstChild._listeners).to.deep.equal({
				animationend: func
			});

			render(
, scratch);
			expect(
				proto.removeEventListener
			).to.have.been.calledOnce.and.to.have.been.calledWithExactly(
				'animationend',
				sinon.match.func,
				false
			);
		});

		it('should support onTransitionEnd', () => {
			const func = () => {};
			render(
, scratch);

			expect(
				proto.addEventListener
			).to.have.been.calledOnce.and.to.have.been.calledWithExactly(
				'transitionend',
				sinon.match.func,
				false
			);

			expect(scratch.firstChild._listeners).to.deep.equal({
				transitionend: func
			});

			render(
, scratch);
			expect(
				proto.removeEventListener
			).to.have.been.calledOnce.and.to.have.been.calledWithExactly(
				'transitionend',
				sinon.match.func,
				false
			);
		});

		it('should support defaultValue', () => {
			render(<input defaultValue="foo" />, scratch);
			expect(scratch.firstElementChild).to.have.property('value', 'foo');
		});

		it('should ignore defaultValue when value is 0', () => {
			render(<input defaultValue={2} value={0} />, scratch);
			expect(scratch.firstElementChild.value).to.equal('0');
		});

		it('should call the callback', () => {
			let spy = sinon.spy();
			render(
, scratch, spy);
			expect(spy).to.be.calledOnce;
			expect(spy).to.be.calledWithExactly();
		});

		// Issue #1727
		it('should destroy the any existing DOM nodes inside the container', () => {
			scratch.appendChild(document.createElement('div'));
			scratch.appendChild(document.createElement('div'));

			render(foo, scratch);
			expect(scratch.innerHTML).to.equal('foo');
		});

		it('should only destroy existing DOM nodes on first render', () => {
			scratch.appendChild(document.createElement('div'));
			scratch.appendChild(document.createElement('div'));

			render(<input />, scratch);

			let child = scratch.firstChild;
			child.focus();
			render(<input />, scratch);
			expect(document.activeElement.nodeName).to.equal('INPUT');
		});
	});

	describe('createFactory', () => {
		it('should create a DOM element', () => {
			render(createFactory('span')({ class: 'foo' }, '1'), scratch);
			expect(scratch.innerHTML).to.equal('1');
		});

		it('should create a component', () => {
			const Foo = ({ id, children }) =>
foo {children}
;
			render(createFactory(Foo)({ id: 'value' }, 'bar'), scratch);
			expect(scratch.innerHTML).to.equal('
foo bar
');
		});
	});

	describe('createElement()', () => {
		it('should be exported', () => {
			expect(React)
				.to.have.property('createElement')
				.that.is.a('function')
				.that.equals(createElement);
		});

		it('should normalize vnodes', () => {
			let vnode = (

					t

			);
			let $$typeof = 0xeac7;
			try {
				// eslint-disable-next-line
				if (
					Function.prototype.toString
						.call(eval('Symbol.for'))
						.match(/\[native code\]/)
				) {
					// eslint-disable-next-line
					$$typeof = eval('Sym' + 'bol.for("react.element")');
				}
			} catch (e) {}
			expect(vnode).to.have.property('$$typeof', $$typeof);
			expect(vnode).to.have.property('type', 'div');
			expect(vnode)
				.to.have.property('props')
				.that.is.an('object');
			expect(vnode.props).to.have.property('children');
			expect(vnode.props.children).to.have.property('$$typeof', $$typeof);
			expect(vnode.props.children).to.have.property('type', 'a');
			expect(vnode.props.children)
				.to.have.property('props')
				.that.is.an('object');
			expect(vnode.props.children.props).to.eql({ children: 't' });
		});

		it('should normalize onChange', () => {
			let props = { onChange() {} };

			function expectToBeNormalized(vnode, desc) {
				expect(vnode, desc)
					.to.have.property('props')
					.with.all.keys(['oninput'].concat(vnode.props.type ? 'type' : []))
					.and.property('oninput')
					.that.is.a('function');
			}

			function expectToBeUnmodified(vnode, desc) {
				expect(vnode, desc)
					.to.have.property('props')
					.eql({
						...props,
						...(vnode.props.type ? { type: vnode.props.type } : {})
					});
			}

			expectToBeUnmodified(
, '
');
			expectToBeUnmodified(
				<input {...props} type="radio" />,
				'<input type="radio">'
			);
			expectToBeUnmodified(
				<input {...props} type="checkbox" />,
				'<input type="checkbox">'
			);
			expectToBeUnmodified(
				<input {...props} type="file" />,
				'<input type="file">'
			);

			expectToBeNormalized(<textarea {...props} />, '<textarea>');
			expectToBeNormalized(<input {...props} />, '<input>');
			expectToBeNormalized(
				<input {...props} type="text" />,
				'<input type="text">'
			);
		});

		it('should normalize beforeinput event listener', () => {
			let spy = sinon.spy();
			render(<input onBeforeInput={spy} />, scratch);
			scratch.firstChild.dispatchEvent(createEvent('beforeinput'));
			expect(spy).to.be.calledOnce;
		});
	});

	describe('Component', () => {
		it('should be exported', () => {
			expect(React)
				.to.have.property('Component')
				.that.is.a('function')
				.that.equals(Component);
		});
	});

	describe('cloneElement', () => {
		it('should clone elements', () => {
			let element = (
				<foo a="b" c="d">
					ab
				</foo>
			);
			expect(cloneElement(element)).to.eql(element);
		});

		it('should support props.children', () => {
			let element = <foo children={b} />;
			let clone = cloneElement(element);
			expect(clone).to.eql(element);
			expect(cloneElement(clone).props.children).to.eql(element.props.children);
		});

		it('children take precedence over props.children', () => {
			let element = (
				<foo children={c}>

b

				</foo>
			);
			let clone = cloneElement(element);
			expect(clone).to.eql(element);
			expect(clone.props.children.type).to.eql('div');
		});

		it('should support children in prop argument', () => {
			let element = <foo />;
			let children = [b];
			let clone = cloneElement(element, { children });
			expect(clone.props.children).to.eql(children);
		});

		it('children argument takes precedence over props.children', () => {
			let element = <foo />;
			let childrenA = [b];
			let childrenB = [
c
];
			let clone = cloneElement(element, { children: childrenA }, ...childrenB);
			expect(clone.props.children).to.eql(childrenB);
		});

		it('children argument takes precedence over props.children even if falsey', () => {
			let element = <foo />;
			let childrenA = [b];
			let clone = cloneElement(element, { children: childrenA }, undefined);
			expect(clone.children).to.eql(undefined);
		});

		it('should skip cloning on invalid element', () => {
			let element = { foo: 42 };
			let clone = cloneElement(element);
			expect(clone).to.eql(element);
		});

		it('should work with jsx constructor from core', () => {
			function Foo(props) {
				return
{props.value}
;
			}

			let clone = cloneElement(preactH(Foo), { value: 'foo' });
			render(clone, scratch);
			expect(scratch.textContent).to.equal('foo');
		});
	});

	describe('findDOMNode()', () => {
		class Helper extends React.Component {
			render({ something }) {
				if (something == null) return null;
				if (something === false) return null;
				return
;
			}
		}

		it.skip('should return DOM Node if render is not false nor null', () => {
			const helper = React.render(<Helper />, scratch);
			expect(findDOMNode(helper)).to.be.instanceof(Node);
		});

		it('should return null if given null', () => {
			expect(findDOMNode(null)).to.be.null;
		}),
			it('should return a regular DOM Element if given a regular DOM Element', () => {
				let scratch = document.createElement('div');
				expect(findDOMNode(scratch)).to.equalNode(scratch);
			}),
			// NOTE: React.render() returning false or null has the component pointing
			// 			to no DOM Node, in contrast, Preact always render an empty Text DOM Node.
			it('should return null if render returns false', () => {
				const helper = React.render(<Helper something={false} />, scratch);
				expect(findDOMNode(helper)).to.be.null;
			});

		// NOTE: React.render() returning false or null has the component pointing
		// 			to no DOM Node, in contrast, Preact always render an empty Text DOM Node.
		it('should return null if render returns null', () => {
			const helper = React.render(<Helper something={null} />, scratch);
			expect(findDOMNode(helper)).to.be.null;
		});
	});

	describe('unmountComponentAtNode', () => {
		it('should unmount a root node', () => {
			const App = () =>
foo
;
			render(<App />, scratch);

			expect(unmountComponentAtNode(scratch)).to.equal(true);
			expect(scratch.innerHTML).to.equal('');
		});

		it('should do nothing if root is not mounted', () => {
			expect(unmountComponentAtNode(scratch)).to.equal(false);
			expect(scratch.innerHTML).to.equal('');
		});
	});

	describe('unstable_batchedUpdates', () => {
		it('should call the callback', () => {
			const spy = sinon.spy();
			unstable_batchedUpdates(spy);
			expect(spy).to.be.calledOnce;
		});

		it('should call callback with only one arg', () => {
			const spy = sinon.spy();
			unstable_batchedUpdates(spy, 'foo', 'bar');
			expect(spy).to.be.calledWithExactly('foo');
		});
	});

	it('should patch events', () => {
		let spy = sinon.spy();
		render(
, scratch);
		scratch.firstChild.click();

		expect(spy).to.be.calledOnce;
		expect(spy.args[0][0]).to.haveOwnProperty('persist');
		expect(spy.args[0][0]).to.haveOwnProperty('nativeEvent');
	});

	it('should normalize ondoubleclick event', () => {
		let vnode =
 null} />;
		expect(vnode.props).to.haveOwnProperty('ondblclick');
	});

	it('should normalize onChange for textarea', () => {
		let vnode = <textarea onChange={() => null} />;
		expect(vnode.props).to.haveOwnProperty('oninput');
		expect(vnode.props).to.not.haveOwnProperty('onchange');

		vnode = <textarea oninput={() => null} onChange={() => null} />;
		expect(vnode.props).to.haveOwnProperty('oninput');
		expect(vnode.props).to.not.haveOwnProperty('onchange');
	});

	it('should not normalize onChange for range', () => {
		render(<input type="range" onChange={() => null} />, scratch);
		expect(scratch.firstChild._listeners).to.haveOwnProperty('change');
		expect(scratch.firstChild._listeners).to.not.haveOwnProperty('input');
	});

	it('should normalize class+className even on components', () => {
		function Foo(props) {
			return (

					foo

			);
		}
		render(<Foo class="foo" />, scratch);
		expect(scratch.firstChild.className).to.equal('foo');
		render(null, scratch);

		render(<Foo className="foo" />, scratch);
		expect(scratch.firstChild.className).to.equal('foo');
	});
});


#people-app {
	position: relative;
	overflow: hidden;
	min-height: 100vh;
	animation: popup 300ms cubic-bezier(0.3, 0.7, 0.3, 1) forwards;
	background: var(--app-background);
	--menu-width: 260px;
	--menu-item-height: 50px;

	@media (min-width: 1280px) {
		max-width: 1280px;
		min-height: calc(100vh - 64px);
		margin: 32px auto;
		border-radius: 10px;
	}

	> nav {
		position: absolute;
		display: flow-root;
		width: var(--menu-width);
		height: 100%;
		background-color: var(--app-background-secondary);
		overflow-x: hidden;
		overflow-y: auto;
	}

	> nav h4 {
		padding-left: 16px;
		font-weight: normal;
		text-transform: uppercase;
	}

	> nav ul {
		position: relative;
	}

	> nav li {
		position: absolute;
		width: 100%;
		animation: zoom 200ms forwards;
		opacity: 0;
		transition: top 200ms;
	}

	> nav li > a {
		position: relative;
		display: flex;
		overflow: hidden;
		flex-flow: row;
		align-items: center;
		margin-left: 16px;
		border-right: 2px solid transparent;
		border-bottom-left-radius: 48px;
		border-top-left-radius: 48px;
		text-transform: capitalize;
		transition: border 500ms;
	}

	> nav li > a:hover {
		background-color: var(--app-highlight);
	}

	> nav li > a::after {
		position: absolute;
		top: 0;
		right: -2px;
		bottom: 0;
		left: 0;
		background-image: radial-gradient(
			circle,
			var(--app-ripple) 1%,
			transparent 1%
		);
		background-position: center;
		background-repeat: no-repeat;
		background-size: 10000%;
		content: '';
		opacity: 0;
		transition: opacity 700ms, background 300ms;
	}

	> nav li > a:active::after {
		background-size: 100%;
		opacity: 0.5;
		transition: none;
	}

	> nav li > a.active {
		border-color: var(--app-primary);
		background-color: var(--app-highlight);
	}

	> nav li > a > * {
		margin: 8px;
	}

	#people-main {
		padding-left: var(--menu-width);
	}
}


// if update() mutates state in-place, skip the copy:

if (typeof update !== 'function' || (update = update(s, this.props))) {

// Skip update if updater function returned null

keep replace replace keep keep keep keep keep

import { EMPTY_OBJ, EMPTY_ARR } from '../constants';
import { Component, enqueueRender } from '../component';
import { Fragment } from '../create-element';
import { diffChildren, toChildArray } from './children';
import { diffProps } from './props';
import { assign, removeNode } from '../util';
import options from '../options';

/**
 * Diff two virtual nodes and apply proper changes to the DOM
 * @param {import('../internal').PreactElement} parentDom The parent of the DOM element
 * @param {import('../internal').VNode} newVNode The new virtual node
 * @param {import('../internal').VNode} oldVNode The old virtual node
 * @param {object} context The current context object
 * @param {boolean} isSvg Whether or not this element is an SVG node
 * @param {Array<import('../internal').PreactElement>} excessDomChildren
 * @param {Array<import('../internal').Component>} commitQueue List of components
 * which have callbacks to invoke in commitRoot
 * @param {Element | Text} oldDom The current attached DOM
 * element any new dom elements should be placed around. Likely `null` on first
 * render (except when hydrating). Can be a sibling DOM element when diffing
 * Fragments that have siblings. In most cases, it starts out as `oldChildren[0]._dom`.
 * @param {boolean} [isHydrating] Whether or not we are in hydration
 */
export function diff(
	parentDom,
	newVNode,
	oldVNode,
	context,
	isSvg,
	excessDomChildren,
	commitQueue,
	oldDom,
	isHydrating
) {
	let tmp,
		newType = newVNode.type;

	// When passing through createElement it assigns the object
	// constructor as undefined. This to prevent JSON-injection.
	if (newVNode.constructor !== undefined) return null;

	if ((tmp = options._diff)) tmp(newVNode);

	try {
		outer: if (typeof newType === 'function') {
			let c, isNew, oldProps, oldState, snapshot, clearProcessingException;
			let newProps = newVNode.props;

			// Necessary for createContext api. Setting this property will pass
			// the context value as `this.context` just for this component.
			tmp = newType.contextType;
			let provider = tmp && context[tmp._id];
			let cctx = tmp
				? provider
					? provider.props.value
					: tmp._defaultValue
				: context;

			// Get component and set it to `c`
			if (oldVNode._component) {
				c = newVNode._component = oldVNode._component;
				clearProcessingException = c._processingException = c._pendingError;
			} else {
				// Instantiate the new component
				if ('prototype' in newType && newType.prototype.render) {
					newVNode._component = c = new newType(newProps, cctx); // eslint-disable-line new-cap
				} else {
					newVNode._component = c = new Component(newProps, cctx);
					c.constructor = newType;
					c.render = doRender;
				}
				if (provider) provider.sub(c);

				c.props = newProps;
				if (!c.state) c.state = {};
				c.context = cctx;
				c._context = context;
				isNew = c._dirty = true;
				c._renderCallbacks = [];
			}

			// Invoke getDerivedStateFromProps
			if (c._nextState == null) {
				c._nextState = c.state;
			}
			if (newType.getDerivedStateFromProps != null) {
				assign(
					c._nextState == c.state
						? (c._nextState = assign({}, c._nextState))
						: c._nextState,
					newType.getDerivedStateFromProps(newProps, c._nextState)
				);
			}

			oldProps = c.props;
			oldState = c.state;

			// Invoke pre-render lifecycle methods
			if (isNew) {
				if (
					newType.getDerivedStateFromProps == null &&
					c.componentWillMount != null
				) {
					c.componentWillMount();
				}

				if (c.componentDidMount != null) {
					c._renderCallbacks.push(c.componentDidMount);
				}
			} else {
				if (
					newType.getDerivedStateFromProps == null &&
					c._force == null &&
					c.componentWillReceiveProps != null
				) {
					c.componentWillReceiveProps(newProps, cctx);
				}

				if (
					!c._force &&
					c.shouldComponentUpdate != null &&
					c.shouldComponentUpdate(newProps, c._nextState, cctx) === false
				) {
					c.props = newProps;
					c.state = c._nextState;
					c._dirty = false;
					c._vnode = newVNode;
					newVNode._dom = oldVNode._dom;
					newVNode._children = oldVNode._children;
					for (tmp = 0; tmp < newVNode._children.length; tmp++) {
						if (newVNode._children[tmp]) {
							newVNode._children[tmp]._parent = newVNode;
						}
					}
					break outer;
				}

				if (c.componentWillUpdate != null) {
					c.componentWillUpdate(newProps, c._nextState, cctx);
				}

				if (c.componentDidUpdate != null) {
					c._renderCallbacks.push(() => {
						c.componentDidUpdate(oldProps, oldState, snapshot);
					});
				}
			}

			c.context = cctx;
			c.props = newProps;
			c.state = c._nextState;

			if ((tmp = options._render)) tmp(newVNode);

			c._dirty = false;
			c._vnode = newVNode;
			c._parentDom = parentDom;

			tmp = c.render(c.props, c.state, c.context);
			let isTopLevelFragment =
				tmp != null && tmp.type == Fragment && tmp.key == null;
			newVNode._children = toChildArray(
				isTopLevelFragment ? tmp.props.children : tmp
			);

			if (c.getChildContext != null) {
				context = assign(assign({}, context), c.getChildContext());
			}

			if (!isNew && c.getSnapshotBeforeUpdate != null) {
				snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);
			}

			diffChildren(
				parentDom,
				newVNode,
				oldVNode,
				context,
				isSvg,
				excessDomChildren,
				commitQueue,
				oldDom,
				isHydrating
			);

			c.base = newVNode._dom;

			if (c._renderCallbacks.length) {
				commitQueue.push(c);
			}

			if (clearProcessingException) {
				c._pendingError = c._processingException = null;
			}

			c._force = null;
		} else {
			newVNode._dom = diffElementNodes(
				oldVNode._dom,
				newVNode,
				oldVNode,
				context,
				isSvg,
				excessDomChildren,
				commitQueue,
				isHydrating
			);
		}

		if ((tmp = options.diffed)) tmp(newVNode);
	} catch (e) {
		options._catchError(e, newVNode, oldVNode);
	}

	return newVNode._dom;
}

/**
 * @param {Array<import('../internal').Component>} commitQueue List of components
 * which have callbacks to invoke in commitRoot
 * @param {import('../internal').VNode} root
 */
export function commitRoot(commitQueue, root) {
	if (options._commit) options._commit(root, commitQueue);

	commitQueue.some(c => {
		try {
			commitQueue = c._renderCallbacks;
			c._renderCallbacks = [];
			commitQueue.some(cb => {
				cb.call(c);
			});
		} catch (e) {
			options._catchError(e, c._vnode);
		}
	});
}

/**
 * Diff two virtual nodes representing DOM element
 * @param {import('../internal').PreactElement} dom The DOM element representing
 * the virtual nodes being diffed
 * @param {import('../internal').VNode} newVNode The new virtual node
 * @param {import('../internal').VNode} oldVNode The old virtual node
 * @param {object} context The current context object
 * @param {boolean} isSvg Whether or not this DOM node is an SVG node
 * @param {*} excessDomChildren
 * @param {Array<import('../internal').Component>} commitQueue List of components
 * which have callbacks to invoke in commitRoot
 * @param {boolean} isHydrating Whether or not we are in hydration
 * @returns {import('../internal').PreactElement}
 */
function diffElementNodes(
	dom,
	newVNode,
	oldVNode,
	context,
	isSvg,
	excessDomChildren,
	commitQueue,
	isHydrating
) {
	let i;
	let oldProps = oldVNode.props;
	let newProps = newVNode.props;

	// Tracks entering and exiting SVG namespace when descending through the tree.
	isSvg = newVNode.type === 'svg' || isSvg;

	if (dom == null && excessDomChildren != null) {
		for (i = 0; i < excessDomChildren.length; i++) {
			const child = excessDomChildren[i];

			if (
				child != null &&
				(newVNode.type === null
					? child.nodeType === 3
					: child.localName === newVNode.type)
			) {
				dom = child;
				excessDomChildren[i] = null;
				break;
			}
		}
	}

	if (dom == null) {
		if (newVNode.type === null) {
			return document.createTextNode(newProps);
		}
		dom = isSvg
			? document.createElementNS('http://www.w3.org/2000/svg', newVNode.type)
			: document.createElement(newVNode.type);
		// we created a new parent, so none of the previously attached children can be reused:
		excessDomChildren = null;
	}

	if (newVNode.type === null) {
		if (excessDomChildren != null)
			excessDomChildren[excessDomChildren.indexOf(dom)] = null;
		if (oldProps !== newProps) {
			dom.data = newProps;
		}
	} else if (newVNode !== oldVNode) {
		if (excessDomChildren != null) {
			excessDomChildren = EMPTY_ARR.slice.call(dom.childNodes);
		}

		oldProps = oldVNode.props || EMPTY_OBJ;

		let oldHtml = oldProps.dangerouslySetInnerHTML;
		let newHtml = newProps.dangerouslySetInnerHTML;

		// During hydration, props are not diffed at all (including dangerouslySetInnerHTML)
		// @TODO we should warn in debug mode when props don't match here.
		if (!isHydrating) {
			if (oldProps === EMPTY_OBJ) {
				oldProps = {};
				for (let i = 0; i < dom.attributes.length; i++) {
					oldProps[dom.attributes[i].name] = dom.attributes[i].value;
				}
			}

			if (newHtml || oldHtml) {
				// Avoid re-applying the same '__html' if it did not changed between re-render
				if (!newHtml || !oldHtml || newHtml.__html != oldHtml.__html) {
					dom.innerHTML = (newHtml && newHtml.__html) || '';
				}
			}
		}

		diffProps(dom, newProps, oldProps, isSvg, isHydrating);

		newVNode._children = newVNode.props.children;

		// If the new vnode didn't have dangerouslySetInnerHTML, diff its children
		if (!newHtml) {
			diffChildren(
				dom,
				newVNode,
				oldVNode,
				context,
				newVNode.type === 'foreignObject' ? false : isSvg,
				excessDomChildren,
				commitQueue,
				EMPTY_OBJ,
				isHydrating
			);
		}

		// (as above, don't diff props during hydration)
		if (!isHydrating) {
			if (
				'value' in newProps &&
				newProps.value !== undefined &&
				newProps.value !== dom.value
			)
				dom.value = newProps.value == null ? '' : newProps.value;
			if (
				'checked' in newProps &&
				newProps.checked !== undefined &&
				newProps.checked !== dom.checked
			)
				dom.checked = newProps.checked;
		}
	}

	return dom;
}

/**
 * Invoke or update a ref, depending on whether it is a function or object ref.
 * @param {object|function} ref
 * @param {any} value
 * @param {import('../internal').VNode} vnode
 */
export function applyRef(ref, value, vnode) {
	try {
		if (typeof ref == 'function') ref(value);
		else ref.current = value;
	} catch (e) {
		options._catchError(e, vnode);
	}
}

/**
 * Unmount a virtual node from the tree and apply DOM changes
 * @param {import('../internal').VNode} vnode The virtual node to unmount
 * @param {import('../internal').VNode} parentVNode The parent of the VNode that
 * initiated the unmount
 * @param {boolean} [skipRemove] Flag that indicates that a parent node of the
 * current element is already detached from the DOM.
 */
export function unmount(vnode, parentVNode, skipRemove) {
	let r;
	if (options.unmount) options.unmount(vnode);

	if ((r = vnode.ref)) {
		applyRef(r, null, parentVNode);
	}

	let dom;
	if (!skipRemove && typeof vnode.type !== 'function') {
		skipRemove = (dom = vnode._dom) != null;
	}

	vnode._dom = vnode._lastDomChild = null;

	if ((r = vnode._component) != null) {
		if (r.componentWillUnmount) {
			try {
				r.componentWillUnmount();
			} catch (e) {
				options._catchError(e, parentVNode);
			}
		}

		r.base = r._parentDom = null;
	}

	if ((r = vnode._children)) {
		for (let i = 0; i < r.length; i++) {
			if (r[i]) unmount(r[i], parentVNode, skipRemove);
		}
	}

	if (dom != null) removeNode(dom);
}

/** The `.render()` method for a PFC backing instance. */
function doRender(props, state, context) {
	return this.constructor(props, context);
}

/**
 * Find the closest error boundary to a thrown error and call it
 * @param {object} error The thrown value
 * @param {import('../internal').VNode} vnode The vnode that threw
 * the error that was caught (except for unmounting when this parameter
 * is the highest parent that was being unmounted)
 * @param {import('../internal').VNode} oldVNode The oldVNode of the vnode
 * that threw, if this VNode threw while diffing
 */
options._catchError = function(error, vnode, oldVNode) {
	/** @type {import('../internal').Component} */
	let component;

	for (; (vnode = vnode._parent); ) {
		if ((component = vnode._component) && !component._processingException) {
			try {
				if (
					component.constructor &&
					component.constructor.getDerivedStateFromError != null
				) {
					component.setState(
						component.constructor.getDerivedStateFromError(error)
					);
				} else if (component.componentDidCatch != null) {
					component.componentDidCatch(error);
				} else {
					continue;
				}
				return enqueueRender((component._pendingError = component));
			} catch (e) {
				error = e;
			}
		}
	}

	throw error;
};


import { createElement as h, render } from 'preact';
import { setupScratch, teardown } from '../../../test/_util/helpers';
import { useImperativeHandle, useRef, useState } from '../../src';
import { setupRerender } from '../../../test-utils';

/** @jsx h */

describe('useImperativeHandle', () => {
	/** @type {HTMLDivElement} */
	let scratch;

	/** @type {() => void} */
	let rerender;

	beforeEach(() => {
		scratch = setupScratch();
		rerender = setupRerender();
	});

	afterEach(() => {
		teardown(scratch);
	});

	it('Mutates given ref', () => {
		let ref;

		function Comp() {
			ref = useRef({});
			useImperativeHandle(ref, () => ({ test: () => 'test' }), []);
			return
Test
;
		}

		render(<Comp />, scratch);
		expect(ref.current).to.have.property('test');
		expect(ref.current.test()).to.equal('test');
	});

	it('calls createHandle after every render by default', () => {
		let ref,
			createHandleSpy = sinon.spy();

		function Comp() {
			ref = useRef({});
			useImperativeHandle(ref, createHandleSpy);
			return
Test
;
		}

		render(<Comp />, scratch);
		expect(createHandleSpy).to.have.been.calledOnce;

		render(<Comp />, scratch);
		expect(createHandleSpy).to.have.been.calledTwice;

		render(<Comp />, scratch);
		expect(createHandleSpy).to.have.been.calledThrice;
	});

	it('calls createHandle only on mount if an empty array is passed', () => {
		let ref,
			createHandleSpy = sinon.spy();

		function Comp() {
			ref = useRef({});
			useImperativeHandle(ref, createHandleSpy, []);
			return
Test
;
		}

		render(<Comp />, scratch);
		expect(createHandleSpy).to.have.been.calledOnce;

		render(<Comp />, scratch);
		expect(createHandleSpy).to.have.been.calledOnce;
	});

	it('Updates given ref when args change', () => {
		let ref,
			createHandleSpy = sinon.spy();

		function Comp({ a }) {
			ref = useRef({});
			useImperativeHandle(
				ref,
				() => {
					createHandleSpy();
					return { test: () => 'test' + a };
				},
				[a]
			);
			return
Test
;
		}

		render(<Comp a={0} />, scratch);
		expect(createHandleSpy).to.have.been.calledOnce;
		expect(ref.current).to.have.property('test');
		expect(ref.current.test()).to.equal('test0');

		render(<Comp a={1} />, scratch);
		expect(createHandleSpy).to.have.been.calledTwice;
		expect(ref.current).to.have.property('test');
		expect(ref.current.test()).to.equal('test1');

		render(<Comp a={0} />, scratch);
		expect(createHandleSpy).to.have.been.calledThrice;
		expect(ref.current).to.have.property('test');
		expect(ref.current.test()).to.equal('test0');
	});

	it('Updates given ref when passed-in ref changes', () => {
		let ref1, ref2;

		/** @type {(arg: any) => void} */
		let setRef;

		/** @type {() => void} */
		let updateState;

		const createHandleSpy = sinon.spy(() => ({
			test: () => 'test'
		}));

		function Comp() {
			ref1 = useRef({});
			ref2 = useRef({});

			const [ref, setRefInternal] = useState(ref1);
			setRef = setRefInternal;

			let [value, setState] = useState(0);
			updateState = () => setState((value + 1) % 2);

			useImperativeHandle(ref, createHandleSpy, []);
			return
Test
;
		}

		render(<Comp a={0} />, scratch);
		expect(createHandleSpy).to.have.been.calledOnce;

		updateState();
		rerender();
		expect(createHandleSpy).to.have.been.calledOnce;

		setRef(ref2);
		rerender();
		expect(createHandleSpy).to.have.been.calledTwice;

		updateState();
		rerender();
		expect(createHandleSpy).to.have.been.calledTwice;

		setRef(ref1);
		rerender();
		expect(createHandleSpy).to.have.been.calledThrice;
	});

	it('should not update ref when args have not changed', () => {
		let ref,
			createHandleSpy = sinon.spy(() => ({ test: () => 'test' }));

		function Comp() {
			ref = useRef({});
			useImperativeHandle(ref, createHandleSpy, [1]);
			return
Test
;
		}

		render(<Comp />, scratch);
		expect(createHandleSpy).to.have.been.calledOnce;
		expect(ref.current.test()).to.equal('test');

		render(<Comp />, scratch);
		expect(createHandleSpy).to.have.been.calledOnce;
		expect(ref.current.test()).to.equal('test');
	});

	it('should not throw with nullish ref', () => {
		function Comp() {
			useImperativeHandle(null, () => ({ test: () => 'test' }), [1]);
			return
Test
;
		}

		expect(() => render(<Comp />, scratch)).to.not.throw();
	});
});


/**
 * A helper to generate innerHTML validation strings containing spans
 * @param {string | number} contents The contents of the span, as a string
 */
export const span = contents => `<span>${contents}</span>`;

/**
 * A helper to generate innerHTML validation strings containing divs
 * @param {string | number} contents The contents of the div, as a string
 */
export const div = contents => `<div>${contents}</div>`;

/**
 * A helper to generate innerHTML validation strings containing p
 * @param {string | number} contents The contents of the p, as a string
 */
export const p = contents => `<p>${contents}</p>`;

/**
 * A helper to generate innerHTML validation strings containing sections
 * @param {string | number} contents The contents of the section, as a string
 */
export const section = contents => `<section>${contents}</section>`;

/**
 * A helper to generate innerHTML validation strings containing uls
 * @param {string | number} contents The contents of the ul, as a string
 */
export const ul = contents => `<ul>${contents}</ul>`;

/**
 * A helper to generate innerHTML validation strings containing ols
 * @param {string | number} contents The contents of the ol, as a string
 */
export const ol = contents => `<ol>${contents}</ol>`;

/**
 * A helper to generate innerHTML validation strings containing lis
 * @param {string | number} contents The contents of the li, as a string
 */
export const li = contents => `<li>${contents}</li>`;

/**
 * A helper to generate innerHTML validation strings containing inputs
 */
export const input = () => `<input type="text">`;

/**
 * A helper to generate innerHTML validation strings containing inputs
 * @param {string | number} contents The contents of the h1
 */
export const h1 = contents => `<h1>${contents}</h1>`;

/**
 * A helper to generate innerHTML validation strings containing inputs
 * @param {string | number} contents The contents of the h2
 */
export const h2 = contents => `<h2>${contents}</h2>`;


import { assign } from './util';
import { EMPTY_ARR } from './constants';
import { createVNode } from './create-element';

/**
 * Clones the given VNode, optionally adding attributes/props and replacing its children.
 * @param {import('./internal').VNode} vnode The virtual DOM element to clone
 * @param {object} props Attributes/props to add when cloning
 * @param {Array<import('./index').ComponentChildren>} rest Any additional arguments will be used as replacement children.
 */
export function cloneElement(vnode, props) {
	props = assign(assign({}, vnode.props), props);
	if (arguments.length > 2) props.children = EMPTY_ARR.slice.call(arguments, 2);
	return createVNode(
		vnode.type,
		props,
		props.key || vnode.key,
		props.ref || vnode.ref
	);
}


if (newType.getDerivedStateFromProps != null) {

c._nextState = assign({}, c._nextState);

import { assign } from './util';
import { diff, commitRoot } from './diff/index';
import options from './options';
import { Fragment } from './create-element';

/**
 * Base Component class. Provides `setState()` and `forceUpdate()`, which
 * trigger rendering
 * @param {object} props The initial component props
 * @param {object} context The initial context from parent components'
 * getChildContext
 */
export function Component(props, context) {
	this.props = props;
	this.context = context;
}

/**
 * Update component state and schedule a re-render.
 * @param {object | ((s: object, p: object) => object)} update A hash of state
 * properties to update with new values or a function that given the current
 * state and props returns a new partial state
 * @param {() => void} [callback] A function to be called once component state is
 * updated
 */
Component.prototype.setState = function(update, callback) {
	// only clone state when copying to nextState the first time.
	let s =
		(this._nextState !== this.state && this._nextState) ||
		(this._nextState = assign({}, this.state));

	// if update() mutates state in-place, skip the copy:
	if (typeof update !== 'function' || (update = update(s, this.props))) {
		assign(s, update);
	}

	// Skip update if updater function returned null
	if (update == null) return;

	if (this._vnode) {
		this._force = false;
		if (callback) this._renderCallbacks.push(callback);
		enqueueRender(this);
	}
};

/**
 * Immediately perform a synchronous re-render of the component
 * @param {() => void} [callback] A function to be called after component is
 * re-rendered
 */
Component.prototype.forceUpdate = function(callback) {
	if (this._vnode) {
		// Set render mode so that we can differentiate where the render request
		// is coming from. We need this because forceUpdate should never call
		// shouldComponentUpdate
		this._force = true;
		if (callback) this._renderCallbacks.push(callback);
		enqueueRender(this);
	}
};

/**
 * Accepts `props` and `state`, and returns a new Virtual DOM tree to build.
 * Virtual DOM is generally constructed via [JSX](http://jasonformat.com/wtf-is-jsx).
 * @param {object} props Props (eg: JSX attributes) received from parent
 * element/component
 * @param {object} state The component's current state
 * @param {object} context Context object, as returned by the nearest
 * ancestor's `getChildContext()`
 * @returns {import('./index').ComponentChildren | void}
 */
Component.prototype.render = Fragment;

/**
 * @param {import('./internal').VNode} vnode
 * @param {number | null} [childIndex]
 */
export function getDomSibling(vnode, childIndex) {
	if (childIndex == null) {
		// Use childIndex==null as a signal to resume the search from the vnode's sibling
		return vnode._parent
			? getDomSibling(vnode._parent, vnode._parent._children.indexOf(vnode) + 1)
			: null;
	}

	let sibling;
	for (; childIndex < vnode._children.length; childIndex++) {
		sibling = vnode._children[childIndex];

		if (sibling != null && sibling._dom != null) {
			// Since updateParentDomPointers keeps _dom pointer correct,
			// we can rely on _dom to tell us if this subtree contains a
			// rendered DOM node, and what the first rendered DOM node is
			return sibling._dom;
		}
	}

	// If we get here, we have not found a DOM node in this vnode's children.
	// We must resume from this vnode's sibling (in it's parent _children array)
	// Only climb up and search the parent if we aren't searching through a DOM
	// VNode (meaning we reached the DOM parent of the original vnode that began
	// the search)
	return typeof vnode.type === 'function' ? getDomSibling(vnode) : null;
}

/**
 * Trigger in-place re-rendering of a component.
 * @param {import('./internal').Component} component The component to rerender
 */
function renderComponent(component) {
	let vnode = component._vnode,
		oldDom = vnode._dom,
		parentDom = component._parentDom;

	if (parentDom) {
		let commitQueue = [];
		let newDom = diff(
			parentDom,
			vnode,
			assign({}, vnode),
			component._context,
			parentDom.ownerSVGElement !== undefined,
			null,
			commitQueue,
			oldDom == null ? getDomSibling(vnode) : oldDom
		);
		commitRoot(commitQueue, vnode);

		if (newDom != oldDom) {
			updateParentDomPointers(vnode);
		}
	}
}

/**
 * @param {import('./internal').VNode} vnode
 */
function updateParentDomPointers(vnode) {
	if ((vnode = vnode._parent) != null && vnode._component != null) {
		vnode._dom = vnode._component.base = null;
		for (let i = 0; i < vnode._children.length; i++) {
			let child = vnode._children[i];
			if (child != null && child._dom != null) {
				vnode._dom = vnode._component.base = child._dom;
				break;
			}
		}

		return updateParentDomPointers(vnode);
	}
}

/**
 * The render queue
 * @type {Array<import('./internal').Component>}
 */
let q = [];

/**
 * Asynchronously schedule a callback
 * @type {(cb) => void}
 */
/* istanbul ignore next */
const defer =
	typeof Promise == 'function'
		? Promise.prototype.then.bind(Promise.resolve())
		: setTimeout;

/*
 * The value of `Component.debounce` must asynchronously invoke the passed in callback. It is
 * important that contributors to Preact can consistently reason about what calls to `setState`, etc.
 * do, and when their effects will be applied. See the links below for some further reading on designing
 * asynchronous APIs.
 * * [Designing APIs for Asynchrony](https://blog.izs.me/2013/08/designing-apis-for-asynchrony)
 * * [Callbacks synchronous and asynchronous](https://blog.ometer.com/2011/07/24/callbacks-synchronous-and-asynchronous/)
 */

let prevDebounce = options.debounceRendering;

/**
 * Enqueue a rerender of a component
 * @param {import('./internal').Component} c The component to rerender
 */
export function enqueueRender(c) {
	if (
		(!c._dirty && (c._dirty = true) && q.push(c) === 1) ||
		prevDebounce !== options.debounceRendering
	) {
		prevDebounce = options.debounceRendering;
		(options.debounceRendering || defer)(process);
	}
}

/** Flush the render queue by rerendering all queued components */
function process() {
	let p;
	q.sort((a, b) => b._vnode._depth - a._vnode._depth);
	while ((p = q.pop())) {
		// forceUpdate's callback argument is reused here to indicate a non-forced update.
		if (p._dirty) renderComponent(p);
	}
}


/* istanbul ignore file */
import {
	getData,
	getChildren,
	getInstance,
	hasDataChanged,
	isRoot
} from './custom';

/**
 * Custom renderer tailored for Preact. It converts updated vnode trees
 * to events the devtools can understand.
 * @class Renderer
 */
export class Renderer {
	constructor(hook, rid) {
		/** @type {string} */
		this.rid = rid;
		this.hook = hook;

		/** @type {Array<import('../internal').DevtoolsEvent>} */
		this.pending = [];

		/**
		 * Store the instance of a vnode. This will be later used to decide if a
		 * vnode needs to be mounted or updated. For components the instance refers
		 * to the actual class instance whereas for dom nodes it refers to the
		 * underlying dom element.
		 * @type {WeakMap<import('../internal').Component | import('../internal').PreactElement | HTMLElement | Text, import('../internal').VNode>}
		 */
		this.inst2vnode = new WeakMap();
		this.connected = false;
	}

	/**
	 * Mark the connection status as ready so that we can proceed
	 * to actually flush events.
	 */
	markConnected() {
		this.connected = true;
		this.flushPendingEvents();
	}

	/**
	 * Flush all queued events
	 */
	flushPendingEvents() {
		if (!this.connected) return;

		let events = this.pending;
		this.pending = [];
		for (let i = 0; i < events.length; i++) {
			let event = events[i];
			this.hook.emit(event.type, event);
		}
	}

	/**
	 * Recursively mount a vnode tree. Note that the devtools expects the tree to
	 * be mounted from the bottom up, otherwise the order will be messed up.
	 * Therefore we mount children prior to mounting the vnode itself.
	 * @param {import('../internal').VNode} vnode
	 */
	mount(vnode) {
		this.inst2vnode.set(getInstance(vnode), vnode);
		let data = getData(vnode);

		/** @type {Array<import('../internal').DevtoolsEvent>} */
		let work = [
			{
				internalInstance: vnode,
				data,
				renderer: this.rid,
				type: 'mount'
			}
		];

		// Children must be mounted first
		if (Array.isArray(data.children)) {
			let stack = data.children.slice();
			let item;
			while ((item = stack.pop()) != null) {
				let children = getChildren(item);
				stack.push(...children);

				this.inst2vnode.set(getInstance(item), item);

				let data = getData(item);

				work.push({
					internalInstance: item,
					data,
					renderer: this.rid,
					type: 'mount'
				});
			}
		}

		for (let i = work.length; --i >= 0; ) {
			this.pending.push(work[i]);
		}

		// Special event if we have a root
		if (isRoot(vnode)) {
			this.pending.push({
				internalInstance: vnode,
				data,
				renderer: this.rid,
				type: 'root'
			});
		}
	}

	/**
	 * Update a vnode tree
	 * @param {import('../internal').VNode} vnode
	 */
	update(vnode) {
		let data = getData(vnode);

		// Children must be updated first
		if (Array.isArray(data.children)) {
			for (let i = 0; i < data.children.length; i++) {
				let child = data.children[i];
				let inst = getInstance(child);

				let prevChild = this.inst2vnode.get(inst);
				if (prevChild == null) this.mount(child);
				else this.update(child);

				// Mutate child to keep referential equality intact
				data.children[i] = this.inst2vnode.get(inst);
			}
		}

		let prev = this.inst2vnode.get(data.publicInstance);

		// The `updateProfileTimes` event is a faster version of `updated` and
		// is processed much quicker inside the devtools extension.
		if (!hasDataChanged(prev, vnode)) {
			// Always assume profiling data has changed. When we skip an event here
			// the devtools element picker will somehow break.
			this.pending.push({
				// This property is only used as an id inside the devtools. The
				// relevant data will be read from `.data` instead which is a
				// normalized structure that every react release adheres to. This
				// makes backwards-compatibility easier instead of relying on internal
				// vnode/fiber shape.
				internalInstance: prev,
				data,
				renderer: this.rid,
				type: 'updateProfileTimes'
			});
			return;
		}

		this.pending.push({
			// This property is only used as an id inside the devtools. The
			// relevant data will be read from `.data` instead which is a
			// normalized structure that every react release adheres to. This
			// makes backwards-compatibility easier instead of relying on internal
			// vnode/fiber shape.
			internalInstance: prev,
			data,
			renderer: this.rid,
			type: 'update'
		});
	}

	/**
	 * Pass a rendered tree to the devtools. At this point elements have already
	 * unmounted, so we don't need to check for removals and only update vs mount
	 * instead.
	 * @param {import('../internal').VNode} vnode
	 */
	handleCommitFiberRoot(vnode) {
		let inst = getInstance(vnode);

		if (this.inst2vnode.has(inst)) this.update(vnode);
		else this.mount(vnode);

		// The devtools checks via the existence of this property if the devtools
		// profiler should be enabled or not. If it is missing from the first root
		// node the "Profiler" tab won't show up.
		/** @type {import('../internal').VNode} */
		let root = null;
		if (isRoot(vnode)) {
			/** @type {*} */
			(vnode).treeBaseDuration = 0;
			root = vnode;
		} else {
			// "rootCommitted" always needs the actual root node for the profiler
			// to be able to collect timings. The `_parent` property will
			// point to a vnode for a root node.
			root = vnode;
			while (root._parent != null) {
				root = root._parent;
			}
		}

		this.pending.push({
			internalInstance: root,
			renderer: this.rid,
			data: getData(root),
			type: 'rootCommitted'
		});

		this.flushPendingEvents();
		return vnode;
	}

	/**
	 * Unmount a vnode recursively. Contrary to mounting or updating unmounting needs
	 * to push the events in parent-first order. Because `options.unmount` is
	 * already fired in parent-first order we don't need to traverse anything here.
	 * @param {import('../internal').VNode} vnode
	 */
	handleCommitFiberUnmount(vnode) {
		let inst = getInstance(vnode);
		this.inst2vnode.delete(inst);

		// Special case when unmounting a root (most prominently caused by webpack's
		// `hot-module-reloading`). If this happens we need to unmount the virtual
		// `Fragment` we're wrapping around each root just for the devtools.

		this.pending.push({
			internalInstance: vnode,
			renderer: this.rid,
			type: 'unmount'
		});
	}

	/**
	 * Get the dom element by a vnode
	 * @param {import('../internal').VNode} vnode
	 * @returns {import('../internal').PreactElement | Text}
	 */
	getNativeFromReactElement(vnode) {
		return vnode._dom;
	}

	/**
	 * Get a vnode by a dom element
	 * @param {import('../internal').PreactElement | Text} dom
	 * @returns {import('../internal').VNode | null}
	 */
	getReactElementFromNative(dom) {
		return this.inst2vnode.get(dom) || null;
	}

	// Unused, but devtools expects it to be there
	/* istanbul ignore next */
	walkTree() {}

	// Unused, but devtools expects it to be there
	/* istanbul ignore next */
	cleanup() {}
}


export default function logger(logStats, logConsole) {
	if (!logStats && !logConsole) {
		return;
	}

	const consoleBuffer = new ConsoleBuffer();

	let calls = {};
	let lock = true;

	function serialize(obj) {
		if (obj instanceof Text) return '#text';
		if (obj instanceof Element) return `<${obj.localName}>`;
		if (obj === document) return 'document';
		return Object.prototype.toString.call(obj).replace(/(^\[object |\]$)/g, '');
	}

	function count(key) {
		if (lock === true) return;
		calls[key] = (calls[key] || 0) + 1;

		if (logConsole) {
			consoleBuffer.log(key);
		}
	}

	function logCall(obj, method, name) {
		let old = obj[method];
		obj[method] = function() {
			let c = '';
			for (let i = 0; i < arguments.length; i++) {
				if (c) c += ', ';
				c += serialize(arguments[i]);
			}
			count(`${serialize(this)}.${method}(${c})`);
			return old.apply(this, arguments);
		};
	}

	logCall(document, 'createElement');
	logCall(document, 'createElementNS');
	logCall(Element.prototype, 'remove');
	logCall(Element.prototype, 'appendChild');
	logCall(Element.prototype, 'removeChild');
	logCall(Element.prototype, 'insertBefore');
	logCall(Element.prototype, 'replaceChild');
	logCall(Element.prototype, 'setAttribute');
	logCall(Element.prototype, 'setAttributeNS');
	logCall(Element.prototype, 'removeAttribute');
	logCall(Element.prototype, 'removeAttributeNS');
	let d =
		Object.getOwnPropertyDescriptor(CharacterData.prototype, 'data') ||
		Object.getOwnPropertyDescriptor(Node.prototype, 'data');
	Object.defineProperty(Text.prototype, 'data', {
		get() {
			let value = d.get.call(this);
			count(`get #text.data`);
			return value;
		},
		set(v) {
			count(`set #text.data`);
			return d.set.call(this, v);
		}
	});

	let root;
	function setup() {
		if (!logStats) return;

		lock = true;
		root = document.createElement('table');
		root.style.cssText =
			'position: fixed; right: 0; top: 0; z-index:999; background: #000; font-size: 12px; color: #FFF; opacity: 0.9; white-space: nowrap;';
		let header = document.createElement('thead');
		header.innerHTML =
			'
Stats <button id="clear-logs">clear</button>
';
		root.tableBody = document.createElement('tbody');
		root.appendChild(root.tableBody);
		root.appendChild(header);
		document.documentElement.appendChild(root);
		let btn = document.getElementById('clear-logs');
		btn.addEventListener('click', () => {
			for (let key in calls) {
				calls[key] = 0;
			}
		});
		lock = false;
	}

	let rows = {};
	function createRow(id) {
		let row = document.createElement('tr');
		row.key = document.createElement('td');
		row.key.textContent = id;
		row.appendChild(row.key);
		row.value = document.createElement('td');
		row.value.textContent = ' ';
		row.appendChild(row.value);
		root.tableBody.appendChild(row);
		return (rows[id] = row);
	}

	function insertInto(parent) {
		parent.appendChild(root);
	}

	function remove() {
		clearInterval(updateTimer);
	}

	function update() {
		if (!logStats) return;

		lock = true;
		for (let i in calls) {
			if (calls.hasOwnProperty(i)) {
				let row = rows[i] || createRow(i);
				row.value.firstChild.nodeValue = calls[i];
			}
		}
		lock = false;
	}

	let updateTimer = setInterval(update, 50);

	setup();
	lock = false;
	return { insertInto, update, remove };
}

/**
 * Logging to the console significantly affects performance.
 * Buffer calls to console and replay them at the end of the
 * current stack
 * @extends {Console}
 */
class ConsoleBuffer {
	constructor() {
		/** @type {Array<[string, any[]]>} */
		this.buffer = [];
		this.deferred = null;

		for (let methodName of Object.keys(console)) {
			this[methodName] = this.proxy(methodName);
		}
	}

	proxy(methodName) {
		return (...args) => {
			this.buffer.push([methodName, args]);
			this.deferFlush();
		};
	}

	deferFlush() {
		if (this.deferred == null) {
			this.deferred = Promise.resolve()
				.then(() => this.flush())
				.then(() => (this.deferred = null));
		}
	}

	flush() {
		let method;
		while ((method = this.buffer.shift())) {
			let [name, args] = method;
			console[name](...args);
		}
	}
}